---[[
--- Marketplace view for MCPHub UI
--- Browse, search and install MCP servers
---]]
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local View = require("mcphub.ui.views.base")
local utils = require("mcphub.utils")

---@class MarketplaceView: View
---@field active_mode "browse"|"details" Current view mode
---@field selected_server MarketplaceItem|nil Currently selected server
---@field cursor_positions table
---@field active_installation_index number Currently selected installation method index
local MarketplaceView = setmetatable({}, {
    __index = View,
})
MarketplaceView.__index = MarketplaceView

function MarketplaceView:new(ui)
    local instance = View:new(ui, "marketplace") -- Create base view with name
    instance = setmetatable(instance, MarketplaceView)

    -- Initialize state
    instance.active_mode = "browse"
    instance.selected_server = nil
    instance.cursor_positions = {
        browse_mode = nil, -- Will store [line, col]
        details_mode = nil, -- Will store [line, col]
    }
    --  Setup initial keymaps (mode-specific keymaps set in setup_active_mode)
    instance.keymaps = {}

    return instance
end

---Extract unique categories and tags from catalog items
---@return string[]
function MarketplaceView:get_available_categories()
    local categories = {}
    local seen = {}

    -- Get items from state
    local items = State.marketplace_state.catalog.items or {}

    for _, item in ipairs(items) do
        -- Add main category
        if item.category and not seen[item.category] then
            seen[item.category] = true
            table.insert(categories, item.category)
        end

        -- Add tags
        if item.tags then
            for _, tag in ipairs(item.tags) do
                if not seen[tag] then
                    seen[tag] = true
                    table.insert(categories, tag)
                end
            end
        end
    end

    -- Sort categories alphabetically
    table.sort(categories)

    return categories
end

--- Filter and sort catalog items
---@param items MarketplaceItem[] List of items to filter and sort
---@return MarketplaceItem[] Filtered and sorted items
function MarketplaceView:filter_and_sort_items(items)
    if not items or #items == 0 then
        return {}
    end

    local filters = State.marketplace_state.filters
    local filtered = items

    -- Apply search filter with ranking
    if filters.search ~= "" and #filters.search > 0 then
        local ranked_items = {}
        local search_text = filters.search:lower()

        -- First pass: collect items with ranks
        for _, item in ipairs(filtered) do
            local rank = 5 -- Default rank (no match)

            if item.name then
                local name_lower = item.name:lower()
                if name_lower == search_text then
                    rank = 1 -- Exact name match
                elseif name_lower:find("^" .. search_text) then
                    rank = 2 -- Name starts with search text
                elseif name_lower:find(search_text) then
                    rank = 3 -- Name contains search text
                end
            end

            -- Check description only if we haven't found a name match
            if rank == 5 and item.description and item.description:lower():find(search_text) then
                rank = 4 -- Description match
            end

            -- Only include items that actually matched
            if rank < 5 then
                table.insert(ranked_items, {
                    item = item,
                    rank = rank,
                })
            end
        end

        -- Sort by rank
        table.sort(ranked_items, function(a, b)
            if a.rank ~= b.rank then
                return a.rank < b.rank
            end
            -- If ranks are equal, sort by name
            return (a.item.name or ""):lower() < (b.item.name or ""):lower()
        end)

        -- Extract just the items
        filtered = vim.tbl_map(function(ranked)
            return ranked.item
        end, ranked_items)
        return filtered
    end

    -- Apply category filter
    if filters.category ~= "" then
        filtered = vim.tbl_filter(function(item)
            -- Match against main category or tags
            return (item.category == filters.category) or (item.tags and vim.tbl_contains(item.tags, filters.category))
        end, filtered)
    end

    -- Sort results
    local sort_funcs = {
        newest = function(a, b)
            return (a.lastCommit or 0) > (b.lastCommit or 0)
        end,
        downloads = function(a, b)
            return #(a.installations or {}) > #(b.installations or {})
        end,
        stars = function(a, b)
            return (a.stars or 0) > (b.stars or 0)
        end,
        name = function(a, b)
            return (a.name or ""):lower() < (b.name or ""):lower()
        end,
    }

    if filters.sort and sort_funcs[filters.sort] then
        table.sort(filtered, sort_funcs[filters.sort])
    end

    return filtered
end

function MarketplaceView:before_enter()
    View.before_enter(self)
    self:setup_active_mode()
end

-- Add this to the MarketplaceView:after_enter() function
function MarketplaceView:after_enter()
    View.after_enter(self)

    -- Restore appropriate cursor position for current mode
    local line_count = vim.api.nvim_buf_line_count(self.ui.buffer)
    if self.active_mode == "browse" and self.cursor_positions.browse_mode then
        local new_pos = {
            math.min(self.cursor_positions.browse_mode[1], line_count),
            self.cursor_positions.browse_mode[2],
        }
        vim.api.nvim_win_set_cursor(0, new_pos)
    elseif self.active_mode == "details" then
        local install_line = self.interactive_lines[1]
        vim.api.nvim_win_set_cursor(0, { install_line and install_line.line or 7, 0 })
    end

    local group = vim.api.nvim_create_augroup("MarketplaceView", { clear = true })
    -- Set up autocmd for visual selection in details mode
    if self.active_mode == "details" then
        -- Clear any existing autocmds for this buffer
        vim.api.nvim_clear_autocmds({
            buffer = self.ui.buffer,
            group = group,
        })
    end
end

function MarketplaceView:setup_active_mode()
    local function enter_detail_mode()
        local cursor = vim.api.nvim_win_get_cursor(0)
        local server = self:get_server_at_line(cursor[1])
        if server then
            self.cursor_positions.browse_mode = cursor
            self.selected_server = server
            self.active_mode = "details"
            self.active_installation_index = 1
            self:setup_active_mode()
            self:draw()
            local install_line = self.interactive_lines[1]
            vim.api.nvim_win_set_cursor(0, { install_line and install_line.line or 7, 0 })
        end
    end
    local function go_to_browse_mode()
        self.cursor_positions.details_mode = vim.api.nvim_win_get_cursor(0)
        self.active_mode = "browse"
        self.selected_server = nil
        self:setup_active_mode()
        self:draw()
        -- Restore browse mode position
        if self.cursor_positions.browse_mode then
            vim.api.nvim_win_set_cursor(0, self.cursor_positions.browse_mode)
        end
    end
    local function install_server()
        local cursor = vim.api.nvim_win_get_cursor(0)
        local type, context = self:get_line_info(cursor[1])

        if type == "install_with_method" and not State:is_server_installed(self.selected_server.id) then
            -- Open installation editor directly
            self:handle_installation_selection(
                context --[[@as { server: MarketplaceItem, installation: MarketplaceInstallation }]]
            )
        elseif type == "uninstall_server" and State:is_server_installed(self.selected_server.id) then
            -- Show confirmation and uninstall
            if context then
                local server_id = context.server.id --[[@as string]]
                utils.confirm_and_delete_server(server_id, function()
                    self.active_installation_index = 1
                end)
            end
        end
    end
    local function cycle_method(forward)
        if self.selected_server and self.selected_server.installations then
            local num_installations = #self.selected_server.installations
            if num_installations >= 1 then
                local is_installed = State:is_server_installed(self.selected_server.id)
                local max_index = is_installed and num_installations + 1 or num_installations

                if forward then
                    self.active_installation_index = (self.active_installation_index % max_index) + 1
                else
                    self.active_installation_index = ((self.active_installation_index - 2) % max_index) + 1
                end

                self:draw()
                -- Keep cursor on install/uninstall line
                if self.interactive_lines and #self.interactive_lines > 0 then
                    for _, line_info in ipairs(self.interactive_lines) do
                        if line_info.type == "install_with_method" or line_info.type == "uninstall_server" then
                            vim.api.nvim_win_set_cursor(0, { line_info.line, 0 })
                            break
                        end
                    end
                end
            end
        end
    end
    if self.active_mode == "browse" then
        self.keymaps = {
            ["/"] = {
                action = function()
                    vim.ui.input({
                        prompt = "Search: ",
                    }, function(input)
                        local trimmed = input and vim.trim(input)
                        if trimmed and #trimmed > 0 then -- Only update if input has content
                            -- When searching, clear category filter
                            local current = State.marketplace_state.filters or {}
                            State:update({
                                marketplace_state = {
                                    filters = {
                                        search = input,
                                        category = "", -- Clear category when searching
                                        sort = current.sort, -- Preserve sort
                                    },
                                },
                            }, "marketplace")
                            self:focus_first_interactive_line()
                        end
                    end)
                end,
                desc = "Search",
            },
            ["s"] = {
                action = function()
                    local sorts = {
                        { text = "Most Stars", value = "stars" },
                        -- { text = "Most Installations", value = "downloads" },
                        { text = "Recently Updated", value = "newest" },
                        { text = "Name (A-Z)", value = "name" },
                    }
                    vim.ui.select(sorts, {
                        prompt = "Sort by:",
                        format_item = function(item)
                            return item.text
                        end,
                    }, function(choice)
                        if choice then
                            State:update({
                                marketplace_state = {
                                    filters = {
                                        sort = choice.value,
                                    },
                                },
                            }, "marketplace")
                        end
                    end)
                end,
                desc = "Sort",
            },
            ["c"] = {
                action = function()
                    local categories = self:get_available_categories()
                    table.insert(categories, 1, "All Categories")

                    vim.ui.select(categories, {
                        prompt = "Filter by category:",
                    }, function(choice)
                        if choice then
                            -- When selecting category, clear search filter
                            local current = State.marketplace_state.filters or {}
                            State:update({
                                marketplace_state = {
                                    filters = {
                                        category = choice ~= "All Categories" and choice or "",
                                        search = "", -- Clear search when filtering by category
                                        sort = current.sort, -- Preserve sort
                                    },
                                },
                            }, "marketplace")
                            self:focus_first_interactive_line()
                        end
                    end)
                end,
                desc = "Category",
            },
            ["<Esc>"] = {
                action = function()
                    -- Only clear filters if any are active
                    local current = State.marketplace_state.filters or {}
                    if current.category or #(current.search or "") > 0 then
                        State:update({
                            marketplace_state = {
                                filters = {
                                    sort = current.sort, -- Preserve sort
                                    search = "",
                                    category = "",
                                },
                            },
                        }, "marketplace")
                    end
                end,
                desc = "Clear filters",
            },
            ["l"] = {
                action = enter_detail_mode,
                desc = "View details",
            },
            ["<Cr>"] = {
                action = enter_detail_mode,
                desc = "View details",
            },
        }
    else
        self.keymaps = {
            ["<Esc>"] = {
                action = go_to_browse_mode,
                desc = "Back to list",
            },
            ["l"] = {
                action = function()
                    cycle_method(true)
                end,
                desc = "Switch method",
            },
            ["h"] = {
                action = function()
                    cycle_method(false)
                end,
                desc = "Switch method",
            },
            ["<Cr>"] = {
                action = install_server,
                desc = "Install/Uninstall",
            },
        }
    end

    -- Add silent keymap aliases for common navigation keys
    if self.active_mode == "browse" then
        -- In browse mode, add silent alias for viewing details
        self:add_keymap_alias("o", "l") -- 'o' as alias for view details
    elseif self.active_mode == "details" then
        -- In details mode, add silent aliases for method cycling
        self:add_keymap_alias("<Tab>", "l") -- Tab as alias for next method
        self:add_keymap_alias("<S-Tab>", "h") -- Shift+Tab as alias for previous method
    end
    self:apply_keymaps()
end

--- Handle installation selection from details mode
---@param context { server: MarketplaceItem, installation: MarketplaceInstallation }
function MarketplaceView:handle_installation_selection(context)
    local server = context.server
    local installation = context.installation

    local ok, config = utils.json_decode(installation.config or "{}")
    -- Create server config object with the server ID
    local server_config = {
        [server.id] = ok and config or {}, -- Use parsed config or empty table
    }

    -- Open the editor with the pre-populated config
    utils.open_server_editor({
        title = "Install " .. server.name .. " (" .. installation.name .. ")",
        placeholder = utils.pretty_json(vim.json.encode(server_config)),
        start_insert = false,
        go_to_placeholder = true, -- Position cursor at first ${} placeholder
        ask_for_source = true,
        virtual_lines = {
            {
                Text.icons.hint
                    .. " ${VARIABLES} will be resolved from config.global_env or environment if not replaced",
                Text.highlights.muted,
            },
            { Text.icons.hint .. " ${cmd: echo 'secret'} will run command and replace ${}", Text.highlights.muted },
        },
        on_success = function()
            -- Switch to main view and browse mode after successful installation
            self.active_mode = "browse"
            self.selected_server = nil
            self.active_installation_index = nil
            self.ui:switch_view("main")
        end,
    })
end

--- Render installation details for a server
---@param installation MarketplaceInstallation
---@param line_offset number
function MarketplaceView:render_installation_details(installation, line_offset)
    local lines = {}
    installation = installation or {}

    -- Configuration preview (no indent)
    if installation.config then
        vim.list_extend(lines, Text.render_json(installation.config))
        table.insert(lines, Text.pad_line(NuiLine()))
    end

    -- Placeholders section (only show if parameters exist)
    if installation.parameters and #installation.parameters > 0 then
        table.insert(lines, Text.pad_line(NuiLine():append("Placeholders", Text.highlights.muted)))

        for _, param in ipairs(installation.parameters) do
            -- Single line: key: placeholder
            local placeholder_line = NuiLine()
            if param.required ~= false then
                placeholder_line:append("* ", Text.highlights.error)
            else
                placeholder_line:append("  ", Text.highlights.error)
            end
            placeholder_line:append(param.key, Text.highlights.json_property)
            placeholder_line:append(": ", Text.highlights.json_punctuation)
            if param.placeholder then
                placeholder_line:append(param.placeholder, Text.highlights.muted)
            end
            table.insert(lines, Text.pad_line(placeholder_line))
        end

        table.insert(lines, Text.pad_line(NuiLine()))
    end

    return lines
end

--- Render current server configuration in details mode
---@param server MarketplaceItem
---@param line_offset number
function MarketplaceView:render_current_server_config(server, line_offset)
    local lines = {}

    local config_manager = require("mcphub.utils.config_manager")
    local server_config = config_manager.get_server_config(server.id, true) or {}

    if server_config then
        -- Render current config as JSON
        vim.list_extend(lines, Text.render_json(vim.json.encode(server_config)))
        table.insert(lines, Text.pad_line(NuiLine()))
    else
        table.insert(lines, Text.pad_line(NuiLine():append("Current configuration not found", Text.highlights.warn)))
        table.insert(lines, Text.pad_line(NuiLine()))
    end

    return lines
end

--- Helper to find server at cursor line
--- @param line number Line number to check
---@return MarketplaceItem|nil
function MarketplaceView:get_server_at_line(line)
    local type, context = self:get_line_info(line)
    if type == "server" and (context and context.id) then
        -- Look up server in catalog by id
        for _, server in ipairs(State.marketplace_state.catalog.items) do
            if server.id == context.id then
                return server
            end
        end
    end
    return nil
end

function MarketplaceView:render_header_controls()
    local lines = {}
    local width = self:get_width() - (Text.HORIZONTAL_PADDING * 2)
    local filters = State.marketplace_state.filters

    -- Create status sections showing current filters and controls
    local left_section = NuiLine()
        :append(Text.icons.sort .. " ", Text.highlights.muted)
        :append("(", Text.highlights.muted)
        :append("s", Text.highlights.keymap)
        :append(")", Text.highlights.muted)
        :append("ort: ", Text.highlights.muted)
        :append(filters.sort == "" and "stars" or filters.sort, Text.highlights.info)
        :append("  ", Text.highlights.muted)
        :append(Text.icons.tag .. " ", Text.highlights.muted)
        :append("(", Text.highlights.muted)
        :append("c", Text.highlights.keymap)
        :append(")", Text.highlights.muted)
        :append("ategory: ", Text.highlights.muted)
        :append(filters.category == "" and "All" or filters.category, Text.highlights.info)

    -- Show filter clear hint if any filters active
    local has_filters = filters.category ~= "" or #(filters.search or "") > 0
    if has_filters then
        left_section
            :append(" (", Text.highlights.muted)
            :append("<Esc>", Text.highlights.keymap)
            :append(" to clear)", Text.highlights.muted)
    end

    local right_section =
        NuiLine():append("/", Text.highlights.keymap):append(" Search: ", Text.highlights.muted):append(
            filters.search == "" and "" or filters.search,
            #(filters.search or "") > 0 and Text.highlights.info or Text.highlights.muted
        )

    -- Calculate padding needed between sections
    local total_content_width = left_section:width() + right_section:width()
    local padding = width - total_content_width

    -- Combine sections with padding
    local controls_line = NuiLine():append(left_section):append(string.rep(" ", padding)):append(right_section)

    table.insert(lines, Text.pad_line(controls_line))

    return lines
end

--- Render a server card for the marketplace
---@param server MarketplaceItem
---@param index number
---@param line_offset number
function MarketplaceView:render_server_card(server, index, line_offset)
    local lines = {}
    local is_installed = State:is_server_installed(server.id)

    -- Create server name section (left part)
    local name_section = NuiLine():append(
        tostring(index) .. ") " .. server.name,
        is_installed and Text.highlights.success or Text.highlights.title
    )

    -- Show checkmark if installed
    if is_installed then
        name_section:append(" ", Text.highlights.muted):append(Text.icons.install, Text.highlights.success)
    end

    -- Create metadata section (right part)
    local meta_section = NuiLine()

    -- Add featured badge if server is featured
    if server.featured then
        meta_section:append(Text.icons.sparkles .. " ", Text.highlights.success)
    end

    -- Show stars and installations count
    meta_section
        :append(Text.icons.favorite, Text.highlights.muted)
        :append(" " .. (server.stars or "0"), Text.highlights.muted)
        :append(" ", Text.highlights.muted)

    -- Calculate padding between name and metadata
    local padding = 2 -- width - (name_section:width() + meta_section:width())

    -- Combine name and metadata with padding
    local title_line = NuiLine():append(name_section):append(string.rep(" ", padding)):append(meta_section)

    -- Track line for server selection (storing only id)
    self:track_line(line_offset, "server", {
        type = "server",
        id = server.id,
        hint = "[<l> Details]",
    })
    table.insert(lines, Text.pad_line(title_line))

    if server.description then
        table.insert(lines, Text.pad_line(NuiLine():append(server.description, Text.highlights.muted)))
    end

    table.insert(lines, Text.pad_line(NuiLine()))
    return lines
end

--- Render marketplace in browse mode
--- @param line_offset number Offset to apply to line numbers for tracking
function MarketplaceView:render_browse_mode(line_offset)
    local lines = {}

    -- Show appropriate state
    local state = State.marketplace_state
    if state.status == "loading" or state.status == "empty" then
        table.insert(lines, Text.pad_line(NuiLine():append("Loading marketplace catalog...", Text.highlights.muted)))
    elseif state.status == "error" then
        table.insert(
            lines,
            Text.pad_line(
                NuiLine()
                    :append(Text.icons.error .. " ", Text.highlights.error)
                    :append("Failed to load marketplace", Text.highlights.error)
            )
        )
    else
        -- Get filtered and sorted items
        local all_items = state.catalog.items or {}
        local filtered_items = self:filter_and_sort_items(all_items)

        -- Show result count if filters are active
        if #(state.filters.search or "") > 0 or state.filters.category ~= "" then
            local count_line = NuiLine()
                :append("Found ", Text.highlights.muted)
                :append(tostring(#filtered_items), Text.highlights.info)
                :append(" of ", Text.highlights.muted)
                :append(tostring(#all_items), Text.highlights.muted)
                :append(" servers", Text.highlights.muted)
            table.insert(lines, Text.pad_line(count_line))
            table.insert(lines, Text.empty_line())
        end

        -- Show filtered catalog
        if #filtered_items == 0 then
            if #all_items == 0 then
                table.insert(
                    lines,
                    Text.pad_line(NuiLine():append("No servers found in marketplace", Text.highlights.muted))
                )
            else
                table.insert(lines, Text.pad_line(NuiLine():append("No matching servers found", Text.highlights.muted)))
            end
        else
            for i, server in ipairs(filtered_items) do
                vim.list_extend(lines, self:render_server_card(server, i, #lines + line_offset + 1))
            end
        end
    end

    local info_line = NuiLine()
    info_line:append(Text.icons.bug .. " Report issues or suggest changes: ", Text.highlights.title)
    info_line:append("https://github.com/ravitemer/mcp-registry", Text.highlights.link)
    table.insert(lines, Text.pad_line(info_line))
    return lines
end

function MarketplaceView:render_details_mode(line_offset)
    local lines = {}
    local server = self.selected_server
    if not server then
        return lines
    end

    -- Description
    if server.description then
        vim.list_extend(lines, vim.tbl_map(Text.pad_line, Text.multiline(server.description, Text.highlights.muted)))
    end

    table.insert(lines, Text.empty_line())

    -- Server info section
    local info_lines = {
        {
            label = "URL      ",
            icon = Text.icons.link,
            value = server.url,
            is_url = true,
        },
        {
            label = "ID       ",
            icon = Text.icons.info,
            value = server.id,
        },
        {
            label = "Author   ",
            icon = Text.icons.octoface,
            value = server.author or "Unknown",
        },
    }

    -- Add category and tags together
    if server.category then
        local category_value = server.category
        if type(server.tags) == "table" and #server.tags > 0 then
            category_value = category_value .. " [" .. table.concat(server.tags, ", ") .. "]"
        end
        table.insert(info_lines, {
            label = "Tags     ",
            icon = Text.icons.tag,
            value = server.category,
            suffix = type(server.tags) == "table"
                    and #server.tags > 0
                    and (" [" .. table.concat(server.tags, ", ") .. "]")
                or nil,
        })
    end

    -- Render info lines
    for _, info in ipairs(info_lines) do
        if info.value then
            local line = NuiLine()
                :append(info.icon .. "  ", Text.highlights.title)
                :append(info.label .. " : ", Text.highlights.muted)
                :append(info.value, info.is_url and Text.highlights.link or info.highlight or Text.highlights.info)

            if info.suffix then
                line:append(info.suffix, Text.highlights.muted)
            end

            table.insert(lines, Text.pad_line(line))
        end
    end
    table.insert(lines, Text.pad_line(NuiLine()))

    -- Install section
    if server.id then
        local is_installed = State:is_server_installed(server.id)

        if is_installed then
            -- Create uninstall button with Current as first tab
            local uninstall_line = NuiLine()
            uninstall_line:append(Text.icons.uninstall .. " Uninstall: ", Text.highlights.error)

            -- Add Current as index 0 (special case)
            uninstall_line:append(" ")
            local current_active = self.active_installation_index == 1
            local current_highlight = current_active and Text.highlights.button_active
                or Text.highlights.button_inactive
            uninstall_line:append(" Current Config ", current_highlight)

            if #server.installations > 0 then
                uninstall_line:append(" " .. Text.icons.vertical_bar, Text.highlights.muted)
                uninstall_line:append(" Available: ", Text.highlights.muted)
            end
            -- Add installation method tabs
            for i, installation in ipairs(server.installations) do
                uninstall_line:append(" ")

                local is_active = self.active_installation_index == (i + 1)
                local tab_highlight = is_active and Text.highlights.button_active or Text.highlights.button_inactive

                uninstall_line:append(" " .. installation.name .. " ", tab_highlight)
            end
            table.insert(lines, Text.pad_line(uninstall_line))

            -- Track the uninstall line for interaction
            self:track_line(#lines + line_offset, "uninstall_server", {
                type = "uninstall_server",
                server = server,
                hint = "[<Cr> Uninstall, <l> Switch Method]",
            })

            table.insert(lines, Text.pad_line(NuiLine()))
            -- local prereq_line = NuiLine()
            -- prereq_line:append("PREVIEW ", Text.highlights.muted)
            -- table.insert(lines, Text.pad_line(prereq_line))
            table.insert(lines, self:divider())

            -- Show details based on active tab
            if self.active_installation_index == 1 then
                -- Show current server configuration when Current tab is active
                vim.list_extend(lines, self:render_current_server_config(server, line_offset))
            else
                -- Show installation details when an installation method is active
                local active_installation = server.installations[self.active_installation_index - 1]
                if active_installation then
                    vim.list_extend(lines, self:render_installation_details(active_installation, line_offset))
                end
            end
        else
            -- Show installation interface
            if server.installations and #server.installations > 0 then
                local active_installation = server.installations[self.active_installation_index]

                -- Create install button with method tabs
                local install_line = NuiLine()
                install_line:append(Text.icons.install .. " Install ", Text.highlights.success)
                install_line:append("using: ", Text.highlights.muted)

                -- Add installation method tabs
                for i, installation in ipairs(server.installations) do
                    if i > 1 then
                        install_line:append(" ")
                    end

                    local is_active = i == self.active_installation_index
                    local tab_highlight = is_active and Text.highlights.button_active or Text.highlights.button_inactive

                    install_line:append(" " .. installation.name .. " ", tab_highlight)
                end

                table.insert(lines, Text.pad_line(install_line))

                -- Track the install line for interaction
                self:track_line(#lines + line_offset, "install_with_method", {
                    type = "install_with_method",
                    server = server,
                    installation = active_installation,
                    hint = "[<Cr> Install, <l> Switch Method]",
                })

                table.insert(lines, Text.pad_line(NuiLine()))

                -- Show prerequisites before divider (single line)
                -- if active_installation.prerequisites and #active_installation.prerequisites > 0 then
                --     local prereq_line = NuiLine()
                --     prereq_line:append("Requires: ", Text.highlights.muted)
                --     prereq_line:append(table.concat(active_installation.prerequisites, ", "), Text.highlights.warn)
                --     table.insert(lines, Text.pad_line(prereq_line))
                -- end

                -- local prereq_line = NuiLine()
                -- prereq_line:append("PREVIEW ", Text.highlights.muted)
                -- table.insert(lines, Text.pad_line(prereq_line))
                table.insert(lines, self:divider())

                -- Show active installation details
                vim.list_extend(lines, self:render_installation_details(active_installation, line_offset))
            else
                table.insert(
                    lines,
                    Text.pad_line(NuiLine():append("No installation methods available", Text.highlights.warn))
                )
            end
        end
    end

    -- Show info links
    local info_line = NuiLine()
    info_line:append("For more information, visit: ", Text.highlights.muted)
    info_line:append(server.url, Text.highlights.muted)
    table.insert(lines, Text.pad_line(info_line))

    local bug_line = NuiLine()
    bug_line:append("Report issues or suggest changes: ", Text.highlights.muted)
    bug_line:append("https://github.com/ravitemer/mcp-registry", Text.highlights.muted)
    table.insert(lines, Text.pad_line(bug_line))
    return lines
end

function MarketplaceView:render()
    -- Handle special states from base view
    if State.setup_state == "failed" or State.setup_state == "in_progress" or State.setup_state == "not_started" then
        return View.render(self)
    end
    -- Get base header
    local lines = self:render_header(false)

    -- Add title/breadcrumb
    if self.active_mode == "browse" then
        -- Render controls
        vim.list_extend(lines, self:render_header_controls())
    elseif self.selected_server then
        local is_installed = State:is_server_installed(self.selected_server.id)
        local breadcrumb = NuiLine():append("Marketplace > ", Text.highlights.muted):append(
            self.selected_server.name,
            is_installed and Text.highlights.success or Text.highlights.title
        )
        if is_installed then
            breadcrumb:append(" ", Text.highlights.muted):append(Text.icons.install, Text.highlights.success)
        end
        if self.selected_server.stars and self.selected_server.stars > 0 then
            breadcrumb:append(
                " (" .. Text.icons.favorite .. " " .. tostring(self.selected_server.stars) .. ")",
                Text.highlights.muted
            )
        end
        table.insert(lines, Text.pad_line(breadcrumb))
    else
        -- Fallback if server not loaded yet
        local breadcrumb = NuiLine():append("Marketplace", Text.highlights.title)
        table.insert(lines, Text.pad_line(breadcrumb))
    end
    table.insert(lines, self:divider())

    -- Calculate line offset from header
    local line_offset = #lines

    -- Render current mode with line offset
    if self.active_mode == "browse" then
        vim.list_extend(lines, self:render_browse_mode(line_offset))
    else
        vim.list_extend(lines, self:render_details_mode(line_offset))
    end

    return lines
end

function MarketplaceView:focus_first_interactive_line()
    vim.schedule(function()
        if self.interactive_lines and #self.interactive_lines > 0 then
            vim.api.nvim_win_set_cursor(0, { self.interactive_lines[1].line, 0 })
        end
    end)
end

return MarketplaceView

function MarketplaceView:before_leave()
    if self.active_mode == "browse" then
        self.cursor_positions.browse_mode = vim.api.nvim_win_get_cursor(0)
    else
        self.cursor_positions.details_mode = vim.api.nvim_win_get_cursor(0)
    end
    if self._visual_keymap_set then
        pcall(vim.api.nvim_buf_del_keymap, self.ui.buffer, "v", "a")
        self._visual_keymap_set = false
    end
    View.before_leave(self)
end

---@brief [[
--- Main dashboard view for MCPHub
--- Shows server status and connected servers
---@brief ]]
local Capabilities = require("mcphub.ui.capabilities")
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local View = require("mcphub.ui.views.base")
local config_manager = require("mcphub.utils.config_manager")
local constants = require("mcphub.utils.constants")
local native = require("mcphub.native")
local renderer = require("mcphub.utils.renderer")
local ui_utils = require("mcphub.utils.ui")
local utils = require("mcphub.utils")

---@class MainView: View
---@field super View
---@field expanded_server string|nil Currently expanded server name
---@field expanded_workspace string|nil Currently expanded workspace port
---@field active_capability CapabilityHandler|nil Currently active capability
---@field cursor_positions {browse_mode: number[]|nil, capability_line: number[]|nil} Cursor positions for different modes
local MainView = setmetatable({}, {
    __index = View,
})
MainView.__index = MainView

function MainView:new(ui)
    local instance = View:new(ui, "main") -- Create base view with name
    instance = setmetatable(instance, MainView)
    -- Initialize state
    instance.expanded_server = nil
    instance.expanded_workspace = nil
    instance.active_capability = nil
    instance.cursor_positions = {
        browse_mode = nil, -- Will store [line, col]
        capability_line = nil, -- Will store [line, col]
    }

    return instance
end

function MainView:show_prompts_view()
    -- Store current cursor position before switching
    self.cursor_positions.browse_mode = vim.api.nvim_win_get_cursor(0)

    -- Switch to prompts capability
    self.active_capability = Capabilities.create_handler("preview", "MCP Servers", { name = "System Prompts" }, self)
    self:setup_active_mode()
    self:draw()
    -- Move to capability's preferred position
    local cap_pos = self.active_capability:get_cursor_position()
    if cap_pos then
        vim.api.nvim_win_set_cursor(0, cap_pos)
    end
end

function MainView:handle_collapse()
    -- Get current line
    local cursor = vim.api.nvim_win_get_cursor(0)
    local line = cursor[1]

    -- Get line info
    local type, context = self:get_line_info(line)

    -- If we're on a server line, handle directly
    if type == "server" and context then
        if context.status == "connected" and self.expanded_server == context.name then
            local server_line = line
            self.expanded_server = nil -- collapse
            self:draw()
            vim.api.nvim_win_set_cursor(0, { server_line, 3 })
            return
        end
    end

    -- If we're on a workspace line, handle directly
    if type == "workspace" and context then
        if self.expanded_workspace == context.port then
            local workspace_line = line
            self.expanded_workspace = nil -- collapse
            self:draw()
            vim.api.nvim_win_set_cursor(0, { workspace_line, 3 })
            return
        end
    end

    -- If we have an expanded server, determine if we're in its scope
    if self.expanded_server then
        local expanded_server_line
        local next_server_line

        -- Find the expanded server's line and next server's line
        for _, tracked in ipairs(self.interactive_lines) do
            if tracked.type == "server" then
                if tracked.context.name == self.expanded_server then
                    expanded_server_line = tracked.line
                elseif expanded_server_line and not next_server_line then
                    -- This is the next server after our expanded one
                    next_server_line = tracked.line
                    break
                end
            end
        end

        -- Check if current line is within expanded server's section:
        -- After expanded server line and before next server (or end if no next server)
        if
            expanded_server_line
            and line > expanded_server_line
            and (not next_server_line or line < next_server_line)
        then
            self.expanded_server = nil
            self:draw()
            vim.api.nvim_win_set_cursor(0, { expanded_server_line, 3 })
            return
        end
    end

    -- If we have an expanded workspace, determine if we're in its scope
    if self.expanded_workspace then
        local expanded_workspace_line
        local next_workspace_line

        -- Find the expanded workspace's line and next workspace's line
        for _, tracked in ipairs(self.interactive_lines) do
            if tracked.type == "workspace" then
                if tracked.context.port == self.expanded_workspace then
                    expanded_workspace_line = tracked.line
                elseif expanded_workspace_line and not next_workspace_line then
                    -- This is the next workspace after our expanded one
                    next_workspace_line = tracked.line
                    break
                end
            end
        end

        -- Check if current line is within expanded workspace's section:
        -- After expanded workspace line and before next workspace (or end if no next workspace)
        if
            expanded_workspace_line
            and line > expanded_workspace_line
            and (not next_workspace_line or line < next_workspace_line)
        then
            self.expanded_workspace = nil
            self:draw()
            vim.api.nvim_win_set_cursor(0, { expanded_workspace_line, 3 })
        end
    end
end

function MainView:handle_custom_instructions(context)
    -- Get current instructions
    local server_config = config_manager.get_server_config(context.server_name) or {}
    local custom_instructions = server_config.custom_instructions or {}
    local text = custom_instructions.text or ""

    -- Open text box using base class method
    ui_utils.multiline_input("Custom Instructions", text, function(content)
        if content ~= text then
            State.hub_instance:update_server_config(context.server_name, {
                custom_instructions = vim.tbl_extend("force", custom_instructions, { text = content }),
            })
            vim.notify("Updated custom instructions for " .. context.server_name, vim.log.levels.INFO)
        end
    end, {
        filetype = "markdown",
        start_insert = false,
        show_footer = false,
    })
end

function MainView:add_server()
    utils.open_server_editor({
        title = "Paste Server Config",
        start_insert = true,
        ask_for_source = true,
    })
end

function MainView:handle_edit()
    -- Get current line
    local cursor = vim.api.nvim_win_get_cursor(0)
    local line = cursor[1]

    local line_type, context = self:get_line_info(line)
    if not line_type or not context then
        return
    end
    local server_name = context.name
    if line_type == "server" then
        local is_native = native.is_native_server(server_name)
        local server_config = config_manager.get_server_config(server_name) or {}
        local config_source = config_manager.get_config_source(server_name)
        local text = utils.pretty_json(vim.json.encode({
            [server_name] = server_config,
        }) or "")
        utils.open_server_editor({
            title = "Edit '" .. server_name .. "' Config",
            is_native = is_native ~= nil,
            old_server_name = server_name,
            config_source = config_source,
            placeholder = text,
            start_insert = false,
            virtual_lines = {
                {
                    Text.icons.hint
                        .. " ${VARIABLES} will be resolved from config.global_env or environment if not replaced",
                    Text.highlights.muted,
                },
                { Text.icons.hint .. " ${cmd: echo 'secret'} will run command and replace ${}", Text.highlights.muted },
            },
        })
    elseif (line_type == "customInstructions") and context then
        self:handle_custom_instructions(context)
    end
end

--- Handle killing a workspace process
---@param context table Workspace context containing workspace details
function MainView:handle_workspace_kill(context)
    if not context or not context.workspace_details then
        return
    end

    local workspace_details = context.workspace_details
    local workspace_name = vim.fn.fnamemodify(workspace_details.cwd, ":t")
    local is_current = context.is_current

    -- -- Prevent killing current workspace
    -- if is_current then
    --     vim.notify("Cannot kill the current workspace. Switch to a different workspace first.", vim.log.levels.WARN)
    --     return
    -- end

    -- Prepare confirmation message with proper highlights
    local ui_utils = require("mcphub.utils.ui")
    local confirm_lines = {}

    -- Header line with highlights
    local header_line = NuiLine()
    header_line:append("Kill MCP Hub process for workspace ", Text.highlights.text)
    header_line:append("'" .. workspace_name .. "'", Text.highlights.warn)
    header_line:append("?", Text.highlights.text)
    table.insert(confirm_lines, header_line)
    table.insert(confirm_lines, "")

    -- Workspace details with highlights
    local port_line = NuiLine()
    port_line:append("Port: ", Text.highlights.muted)
    port_line:append(tostring(workspace_details.port), Text.highlights.info)
    table.insert(confirm_lines, port_line)

    local pid_line = NuiLine()
    pid_line:append("PID: ", Text.highlights.muted)
    pid_line:append(tostring(workspace_details.pid), Text.highlights.info)
    table.insert(confirm_lines, pid_line)

    local path_line = NuiLine()
    path_line:append("Path: ", Text.highlights.muted)
    path_line:append(workspace_details.cwd, Text.highlights.info)
    table.insert(confirm_lines, path_line)

    table.insert(confirm_lines, "")

    -- Warning with highlights
    local warning_line = NuiLine()
    warning_line:append("This will terminate the hub process.", Text.highlights.error)
    table.insert(confirm_lines, warning_line)
    local async = require("plenary.async")

    async.run(function()
        local confirmed, cancelled = ui_utils.confirm(confirm_lines, {
            min_width = 60,
            max_width = 80,
        })

        if confirmed and not cancelled then
            -- Kill the process
            local success = pcall(function()
                local uv = vim.loop or vim.uv
                uv.kill(workspace_details.pid, "sigterm")
            end)

            if success then
                vim.notify(
                    string.format("Sent SIGTERM to workspace '%s' (PID: %d)", workspace_name, workspace_details.pid),
                    vim.log.levels.INFO
                )
            else
                vim.notify(
                    string.format("Failed to kill workspace '%s' (PID: %d)", workspace_name, workspace_details.pid),
                    vim.log.levels.ERROR
                )
            end
        end
    end, function() end)
end

--- Handle changing directory to a workspace
function MainView:handle_workspace_change_directory()
    -- Get current line
    local cursor = vim.api.nvim_win_get_cursor(0)
    local line = cursor[1]

    local type, context = self:get_line_info(line)
    if not type or not context or type ~= "workspace" then
        return
    end

    local workspace_details = context.workspace_details
    local target_dir = workspace_details.cwd
    local workspace_name = vim.fn.fnamemodify(target_dir, ":t")

    -- Change directory
    local success, err = pcall(vim.cmd.cd, target_dir)
    if success then
        if State.config.workspace.enabled then
            vim.notify(string.format("Changed directory to '%s' (%s)", workspace_name, target_dir), vim.log.levels.INFO)
            -- Trigger directory change handler to potentially switch hubs
            if State.hub_instance and State.hub_instance.handle_directory_change then
                State.hub_instance:handle_directory_change()
            end
        else
            vim.notify("Workspace feature is disabled, directory change won't trigger hub switch", vim.log.levels.WARN)
        end
    else
        vim.notify(string.format("Failed to change directory: %s", err), vim.log.levels.ERROR)
    end
end

function MainView:handle_delete()
    -- Get current line
    local cursor = vim.api.nvim_win_get_cursor(0)
    local line = cursor[1]

    local type, context = self:get_line_info(line)
    if not type or not context then
        return
    end
    if type == "server" then
        local server_name = context.name
        local is_native = native.is_native_server(server_name)
        if is_native then
            return vim.notify("Native servers cannot be deleted, only their configuration can be edited")
        end
        utils.confirm_and_delete_server(server_name)
    elseif type == "workspace" and context then
        self:handle_workspace_kill(context)
    end
end

function MainView:handle_action(line_override, context_override)
    local go_to_cap_line = false
    -- Get current line
    local cursor = vim.api.nvim_win_get_cursor(0)
    local line = line_override or cursor[1]

    -- Get line info from either override or current line
    local type, context
    if context_override then
        type = context_override.type
        context = context_override.context
    else
        type, context = self:get_line_info(line)
    end
    if not type or not context then
        return
    end
    if type == "breadcrumb" then
        self:show_prompts_view()
    elseif type == "server" then
        -- Toggle expand/collapse for server
        if context.status == "connected" then
            if self.expanded_server == context.name then
                self.expanded_server = nil -- collapse
                self:draw()
            else
                self.expanded_server = context.name -- expand
                self:draw()

                -- Find server and capabilities in new view
                local server_line = nil
                local first_cap_line = nil

                for _, tracked in ipairs(self.interactive_lines) do
                    if tracked.type == "server" and tracked.context.name == context.name then
                        server_line = tracked.line
                    elseif
                        tracked.type == "tool"
                        or tracked.type == "resource"
                        or tracked.type == "resourceTemplate"
                        or tracked.type == "customInstructions"
                    then
                        if tracked.context.server_name == context.name and not first_cap_line then
                            first_cap_line = tracked.line
                            break
                        end
                    end
                end

                -- Position cursor:
                -- 1. On first capability if exists
                -- 2. Otherwise on server line
                -- 3. Fallback to current line
                if first_cap_line and go_to_cap_line then
                    vim.api.nvim_win_set_cursor(0, { first_cap_line, 3 })
                elseif server_line then
                    vim.api.nvim_win_set_cursor(0, { server_line, 3 })
                else
                    vim.api.nvim_win_set_cursor(0, { line, 3 })
                end
            end
        elseif context.status == "unauthorized" then
            local authUrl = State.hub_instance:get_server(context.name)["authorizationUrl"]
            if not authUrl then
                return vim.notify("No authorization URL found for server " .. context.name, vim.log.levels.ERROR)
            end
            ---Auto opens the browser with authorization URL
            State.hub_instance:authorize_mcp_server(context.name)
            -- Show popup for the user
            ui_utils.open_auth_popup(context.name, authUrl)
        end
    elseif type == "create_server" then
        -- Store browse mode position before switching
        self.cursor_positions.browse_mode = vim.api.nvim_win_get_cursor(0)

        -- Switch to createServer or addServer capability
        if type == "create_server" then
            self.active_capability =
                Capabilities.create_handler("createServer", "Native Servers", { name = "Create Server" }, self)
        end
        self:setup_active_mode()
        self:draw()
        -- Move to capability's preferred position
        local cap_pos = self.active_capability:get_cursor_position()
        if cap_pos then
            vim.api.nvim_win_set_cursor(0, cap_pos)
        end
    elseif (type == "tool" or type == "resource" or type == "resourceTemplate" or type == "prompt") and context then
        if context.disabled then
            return
        end
        -- Store browse mode position before entering capability
        self.cursor_positions.browse_mode = vim.api.nvim_win_get_cursor(0)

        -- Create capability handler and switch to capability mode
        self.active_capability = Capabilities.create_handler(type, context.server_name, context, self)
        self:setup_active_mode()
        self:draw()

        -- Move to capability's preferred position
        local cap_pos = self.active_capability:get_cursor_position()
        if cap_pos then
            vim.api.nvim_win_set_cursor(0, cap_pos)
        end
    elseif (type == "customInstructions") and context then
        self:handle_custom_instructions(context)
    elseif type == "add_server" then
        self:add_server()
    elseif type == "workspace" and context then
        -- Toggle expand/collapse for workspace
        if self.expanded_workspace == context.port then
            self.expanded_workspace = nil -- collapse
            self:draw()
        else
            self.expanded_workspace = context.port -- expand
            self:draw()

            -- Find workspace line in new view and position cursor
            for _, tracked in ipairs(self.interactive_lines) do
                if tracked.type == "workspace" and tracked.context.port == context.port then
                    vim.api.nvim_win_set_cursor(0, { tracked.line, 3 })
                    break
                end
            end
        end
    end
end

function MainView:handle_cursor_move()
    -- Clear previous highlight
    if self.cursor_highlight then
        vim.api.nvim_buf_del_extmark(self.ui.buffer, self.hover_ns, self.cursor_highlight)
        self.cursor_highlight = nil
    end

    -- Get current line
    local cursor = vim.api.nvim_win_get_cursor(0)
    local line = cursor[1]
    if self.active_capability then
        self.active_capability:handle_cursor_move(self, line)
    else
        -- Get line info
        local type, context = self:get_line_info(line)
        if type then
            -- Add virtual text without line highlight
            self.cursor_highlight = vim.api.nvim_buf_set_extmark(self.ui.buffer, self.hover_ns, line - 1, 0, {
                virt_text = { { context and context.hint or "[<l> Interact]", Text.highlights.muted } },
                virt_text_pos = "eol",
            })
        end
    end
end

function MainView:setup_active_mode()
    if self.active_capability then
        self.keymaps = {
            ["l"] = {
                action = function()
                    if self.active_capability.handle_action then
                        self.active_capability:handle_action(vim.api.nvim_win_get_cursor(0)[1])
                    end
                end,
                desc = "Execute/Submit",
            },
            ["o"] = {
                action = function()
                    ---@diagnostic disable-next-line: undefined-field
                    if self.active_capability.handle_text_box then
                        ---@diagnostic disable-next-line: undefined-field
                        self.active_capability:handle_text_box(vim.api.nvim_win_get_cursor(0)[1])
                    end
                end,
                desc = "Open text box",
            },
            ["<Tab>"] = {
                action = function()
                    ---@diagnostic disable-next-line: undefined-field
                    if self.active_capability.handle_tab then
                        ---@diagnostic disable-next-line: undefined-field
                        self.active_capability:handle_tab()
                    end
                end,
                desc = "Switch tab",
            },
            ["h"] = {
                action = function()
                    -- -- Store capability line before exiting
                    -- self.cursor_positions.capability_line = vim.api.nvim_win_get_cursor(0)

                    -- Clear active capability
                    self.active_capability = nil

                    -- Setup browse mode and redraw
                    self:setup_active_mode()
                    self:draw()

                    -- Restore to last browse mode position
                    if self.cursor_positions.browse_mode then
                        vim.api.nvim_win_set_cursor(0, self.cursor_positions.browse_mode)
                    end
                end,
                desc = "Back",
            },
        }
    else
        -- Normal mode keymaps
        self.keymaps = {
            ["e"] = {
                action = function()
                    self:handle_edit()
                end,
                desc = "Edit",
            },
            ["d"] = {
                action = function()
                    self:handle_delete()
                end,
                desc = "Delete",
            },

            ["A"] = {
                action = function()
                    -- Handle like an add_server action
                    self:add_server()
                end,
                desc = "Add Server",
            },
            ["t"] = {
                action = function()
                    self:handle_server_toggle()
                end,
                desc = "Toggle",
            },
            ["a"] = {
                action = function()
                    self:handle_auto_approve_toggle()
                end,
                desc = "Auto-approve",
            },
            ["h"] = {
                action = function()
                    self:handle_collapse()
                end,
                desc = "Collapse",
            },
            ["l"] = {
                action = function()
                    self:handle_action()
                end,
                desc = "Expand",
            },
            ["gd"] = {
                action = function()
                    self:show_prompts_view()
                end,
                desc = "Preview",
            },
            ["gc"] = {
                action = function()
                    self:handle_workspace_change_directory()
                end,
                desc = "Change Directory",
            },
        }
    end
    self.keymaps["ga"] = {
        action = function()
            vim.g.mcphub_auto_approve = not vim.g.mcphub_auto_approve
            self:draw()
        end,
        desc = "Toggle AutoApprove",
    }

    -- Add silent keymap aliases for common expand/action keys
    if self.active_capability then
        self:add_keymap_alias("<CR>", "l")
        self:add_keymap_alias("<Esc>", "h")
    else
        -- In browse mode, add silent aliases for expand action
        self:add_keymap_alias("<CR>", "l") -- Enter key as alias for expand
        self:add_keymap_alias("o", "l") -- 'o' key as alias for expand
    end

    self:apply_keymaps()
end

-- Helper function to get all tool names for a server
local function get_server_tool_names(server_name)
    local tools = {}

    -- Check if it's a native server
    local is_native = native.is_native_server(server_name)
    if is_native then
        local native_server = is_native
        for _, tool in ipairs(native_server.capabilities.tools or {}) do
            table.insert(tools, tool.name)
        end
    else
        -- Regular MCP server
        for _, server in ipairs(State.server_state.servers) do
            if server.name == server_name and server.capabilities then
                for _, tool in ipairs(server.capabilities.tools or {}) do
                    table.insert(tools, tool.name)
                end
                break
            end
        end
    end

    return tools
end

-- Helper function to determine actual auto-approval status
local function get_auto_approval_status(server_config, all_tools)
    local auto_approve = server_config.autoApprove

    if auto_approve == true then
        return "all", #all_tools -- All tools auto-approved
    elseif type(auto_approve) == "table" and vim.islist(auto_approve) then
        if #auto_approve == 0 then
            return "none", 0
        elseif #auto_approve == #all_tools then
            return "all", #all_tools -- All tools are in the list
        else
            return "partial", #auto_approve -- Some tools auto-approved
        end
    else
        return "none", 0 -- No auto-approval
    end
end

function MainView:handle_auto_approve_toggle()
    -- Get current line
    local cursor = vim.api.nvim_win_get_cursor(0)
    local line = cursor[1]

    -- Get line info
    local type, context = self:get_line_info(line)
    if not type or not context or not State.hub_instance then
        return
    end

    if type == "server" then
        -- Check if server is enabled
        if context.status == "disabled" or context.status == "disconnected" then
            return
        end

        -- Toggle auto-approval for entire server
        local server_name = context.name
        local server_config = config_manager.get_server_config(server_name) or {}

        local all_tools = get_server_tool_names(server_name)
        local status, _ = get_auto_approval_status(server_config, all_tools)
        local new_auto_approve

        if status == "all" then
            -- Currently auto-approving all tools, turn off
            new_auto_approve = {}
        else
            -- Currently partial or no auto-approval, enable for all
            new_auto_approve = vim.deepcopy(all_tools)
        end

        State.hub_instance:update_server_config(server_name, {
            autoApprove = new_auto_approve,
        })
    elseif type == "tool" and context then
        -- Check if tool is enabled
        if context.disabled then
            return
        end

        -- Toggle auto-approval for specific tool
        local server_name = context.server_name
        local tool_name = context.def.name
        local server_config = config_manager.get_server_config(server_name) or {}

        local current_auto_approve = server_config.autoApprove or {}
        local new_auto_approve

        -- Handle boolean case (convert to array first)
        if current_auto_approve == true then
            local all_tools = get_server_tool_names(server_name)
            current_auto_approve = vim.deepcopy(all_tools)
        end

        -- Ensure it's an array
        if not vim.islist(current_auto_approve) then
            current_auto_approve = {}
        end

        -- Toggle the tool in the list using filter instead of table.remove
        local tool_approved = vim.tbl_contains(current_auto_approve, tool_name)

        if tool_approved then
            -- Remove tool from auto-approve list
            new_auto_approve = vim.tbl_filter(function(tool)
                return tool ~= tool_name
            end, current_auto_approve)
        else
            -- Add tool to auto-approve list
            new_auto_approve = vim.deepcopy(current_auto_approve)
            table.insert(new_auto_approve, tool_name)
        end

        State.hub_instance:update_server_config(server_name, {
            autoApprove = new_auto_approve,
        })
    end
end
function MainView:handle_server_toggle()
    -- Get current line
    local cursor = vim.api.nvim_win_get_cursor(0)
    local line = cursor[1]

    -- Get line info
    local type, context = self:get_line_info(line)
    if type == "server" and context and State.hub_instance then
        -- Handle regular MCP server
        -- Gets updated via sse endpoint after file changed rather than explicitly send curl request
        if context.status == "disabled" then
            State.hub_instance:start_mcp_server(context.name, {
                -- via_curl_request = true,
                -- callback = function(response, err)
                --     if err then
                --         vim.notify("Failed to enable server: " .. err, vim.log.levels.ERROR)
                --     end
                -- end,
            })
        else
            State.hub_instance:stop_mcp_server(context.name, true, {
                -- via_curl_request = true,
                -- callback = function(response, err)
                --     if err then
                --         vim.notify("Failed to disable server: " .. err, vim.log.levels.ERROR)
                --     end
                -- end,
            })
        end
    elseif
        (type == "tool" or type == "resource" or type == "resourceTemplate" or type == "prompt")
        and context
        and State.hub_instance
    then
        local server_name = context.server_name
        local server_config = config_manager.get_server_config(server_name) or {}

        local type_config = {
            tool = { id_field = "name", config_field = "disabled_tools" },
            resource = { id_field = "uri", config_field = "disabled_resources" },
            resourceTemplate = { id_field = "uriTemplate", config_field = "disabled_resourceTemplates" },
            prompt = { id_field = "name", config_field = "disabled_prompts" },
        }

        local config = type_config[type]
        local capability_id = context.def[config.id_field]
        local disabled_list = vim.deepcopy(server_config[config.config_field] or {})
        local is_disabled = vim.tbl_contains(disabled_list, capability_id)

        -- Update disabled list based on desired state
        if is_disabled then
            for i, item_id in ipairs(disabled_list) do
                if item_id == capability_id then
                    table.remove(disabled_list, i)
                    break
                end
            end
        else
            table.insert(disabled_list, capability_id)
        end

        -- Update server config with new disabled list
        local updates = {}
        updates[config.config_field] = disabled_list
        State.hub_instance:update_server_config(server_name, updates)
        State:emit(type .. "_list_changed", {
            server_name = server_name,
            config_field = config.config_field,
            disabled_list = disabled_list,
        })
    elseif type == "customInstructions" and context then
        -- Toggle custom instructions state
        local server_name = context.server_name
        local server_config = config_manager.get_server_config(server_name) or {}
        local custom_instructions = server_config.custom_instructions or {}
        local is_disabled = custom_instructions.disabled

        State.hub_instance:update_server_config(server_name, {
            custom_instructions = {
                disabled = not is_disabled,
            },
        })
    end
end

function MainView:get_initial_cursor_position()
    -- Position after server status section
    local lines = self:render_header(false)
    -- vim.list_extend(lines, self:render_hub_status(self:get_width()))
    -- In browse mode, restore last browse position
    if not self.active_capability and self.cursor_positions.browse_mode then
        return self.cursor_positions.browse_mode[1]
    end
    return #lines + 1
end

--- Sort servers by status (connected first, then disconnected, disabled last) and alphabetically within each group
---@param servers table[] List of servers to sort
local function sort_servers(servers)
    table.sort(servers, function(a, b)
        -- First compare status priority
        local status_priority = {
            connected = 1,
            disconnected = 2,
            disabled = 3,
        }
        local a_priority = status_priority[a.status] or 2 -- default to disconnected priority
        local b_priority = status_priority[b.status] or 2

        if a_priority ~= b_priority then
            return a_priority < b_priority
        end

        -- If same status, sort alphabetically
        return a.name < b.name
    end)
    return servers
end

--- Render a server section
---@param title? string Section title
---@param servers table[] List of servers
---@param current_line number Current line number
---@return NuiLine[], number Lines and new current line
function MainView:render_servers_section(title, servers, current_line)
    local lines = {}

    if title then
        -- Section header
        table.insert(lines, Text.pad_line(NuiLine():append(title, Text.highlights.title)))
        current_line = current_line + 1
    end

    -- If no servers in section
    if not servers or #servers == 0 then
        table.insert(
            lines,
            Text.pad_line(
                NuiLine():append(" No servers connected " .. "(Install from Marketplace)", Text.highlights.muted)
            )
        )
        table.insert(lines, Text.empty_line())
        return lines, current_line + 2
    end

    -- Sort and render servers
    local sorted = sort_servers(vim.deepcopy(servers))
    for _, server in ipairs(sorted) do
        local server_config = config_manager.get_server_config(server) or {}
        current_line =
            renderer.render_server_capabilities(server, lines, current_line, server_config, self, server.is_native)
    end

    return lines, current_line
end

--- Render all server sections
---@return NuiLine[]
function MainView:render_servers(line_offset)
    local lines = {}
    local current_line = line_offset

    local width = self:get_width() - (Text.HORIZONTAL_PADDING * 2)
    -- Start with top-level MCP Servers header
    local left_section = NuiLine():append("MCP Servers", Text.highlights.title)

    -- Add token count on MCP Servers section if connected
    if State:is_connected() and State.hub_instance and State.hub_instance:is_ready() then
        local prompts = State.hub_instance:generate_prompts()
        if prompts then
            -- Calculate total tokens from all prompts
            local active_servers_tokens = utils.calculate_tokens(prompts.active_servers or "")
            local use_mcp_tool_tokens = utils.calculate_tokens(prompts.use_mcp_tool or "")
            local access_mcp_resource_tokens = utils.calculate_tokens(prompts.access_mcp_resource or "")
            local total_tokens = active_servers_tokens + use_mcp_tool_tokens + access_mcp_resource_tokens

            if total_tokens > 0 then
                left_section:append(
                    " (~ " .. utils.format_token_count(total_tokens) .. " tokens)",
                    Text.highlights.muted
                )
            end
        end
    end
    local is_auto_approve_enabled = vim.g.mcphub_auto_approve == true
    local right_section = NuiLine()
        :append(Text.icons.auto, is_auto_approve_enabled and Text.highlights.success or Text.highlights.muted)
        :append(" AutoApprove: ", Text.highlights.muted)
        :append(
            is_auto_approve_enabled and "ON" or "OFF",
            is_auto_approve_enabled and Text.highlights.success or Text.highlights.muted
        )
    right_section
        :append(" | ", Text.highlights.muted)
        :append(Text.icons.tower, Text.highlights.success)
        :append(" Port: ", Text.highlights.success)
        :append(tostring(State.current_hub.port or "N/A"), Text.highlights.success)

    -- Calculate padding needed between sections
    local total_content_width = left_section:width() + right_section:width()
    local padding = width - total_content_width

    -- Combine sections with padding
    local header_line = NuiLine():append(left_section):append(string.rep(" ", padding)):append(right_section)

    table.insert(lines, Text.pad_line(header_line))
    current_line = current_line + 1
    -- Track breadcrumb line for interaction
    -- self:track_line(current_line, "breadcrumb", {
    --     hint = "<CR> to preview servers prompt",
    -- })
    table.insert(lines, self:divider())
    current_line = current_line + 1

    current_line = renderer.render_servers_grouped(State.server_state.servers, lines, current_line, self)

    -- -- Add server creation options
    -- table.insert(
    --     lines,
    --     Text.pad_line(
    --         NuiLine()
    --             :append(" " .. Text.icons.plus .. " ", Text.highlights.muted)
    --             :append("Add Server (A)", Text.highlights.muted)
    --     )
    -- )
    -- -- Track line for interaction
    -- self:track_line(current_line + 1, "add_server", {
    --     name = "Add Server",
    --     hint = "[<l> Open Editor]",
    -- })
    -- current_line = current_line + 1

    -- Add spacing between sections
    table.insert(lines, Text.empty_line())
    current_line = current_line + 1

    -- Native servers section header
    table.insert(lines, Text.pad_line(NuiLine():append("" .. " Native Servers", Text.highlights.title)))
    current_line = current_line + 1

    -- Render Native servers first
    local native_lines, native_line = self:render_servers_section(
        nil, -- No title since we added it above
        State.server_state.native_servers,
        current_line
    )
    vim.list_extend(lines, native_lines)
    current_line = native_line

    -- Add create server option
    -- table.insert(lines, Text.empty_line())
    table.insert(
        lines,
        Text.pad_line(
            NuiLine()
                :append(" " .. Text.icons.edit .. " ", Text.highlights.muted)
                :append("Auto-create Server", Text.highlights.muted)
        )
    )
    -- Track line for interaction
    self:track_line(current_line + 1, "create_server", {
        hint = "[<l> Create Server]",
    })

    return lines
end

function MainView:before_enter()
    View.before_enter(self)
    self:setup_active_mode()
end

function MainView:after_enter()
    View.after_enter(self)

    local line_count = vim.api.nvim_buf_line_count(self.ui.buffer)
    -- Restore appropriate cursor position
    if self.active_capability then
        local cap_pos = self.cursor_positions.capability_line or self.active_capability:get_cursor_position()
        if cap_pos then
            local new_pos = { math.min(cap_pos[1], line_count), cap_pos[2] }
            vim.api.nvim_win_set_cursor(0, new_pos)
        end
    else
        -- In browse mode, restore last browse position with column
        if self.cursor_positions.browse_mode then
            local new_pos = {
                math.min(self.cursor_positions.browse_mode[1], line_count),
                self.cursor_positions.browse_mode[2] or 2,
            }
            vim.api.nvim_win_set_cursor(0, new_pos)
        end
    end
end

function MainView:before_leave()
    -- Store appropriate position based on current mode
    if self.active_capability then
        -- In capability mode, store full position
        self.cursor_positions.capability_line = vim.api.nvim_win_get_cursor(0)
    else
        -- In browse mode, store full position
        self.cursor_positions.browse_mode = vim.api.nvim_win_get_cursor(0)
    end

    View.before_leave(self)
end

function MainView:should_show_logs()
    return not vim.tbl_contains({ constants.HubState.READY, constants.HubState.RESTARTED }, State.server_state.state)
end

function MainView:render()
    -- Handle special states from base view
    if State.setup_state == "failed" or State.setup_state == "in_progress" or State.setup_state == "not_started" then
        return View.render(self)
    end
    -- Get base header
    local lines = self:render_header(false)
    if self:should_show_logs() then
        local state_info = renderer.get_hub_info(State.server_state.state)
        local breadcrumb_line = NuiLine():append(state_info.icon, state_info.hl):append(state_info.desc, state_info.hl)
        table.insert(lines, Text.pad_line(breadcrumb_line))
        table.insert(lines, self:divider())
        vim.list_extend(lines, renderer.render_server_entries(State.server_output.entries))
        local errors = renderer.render_hub_errors(nil, false)
        if #errors > 0 then
            vim.list_extend(lines, errors)
        end
        return lines
    end
    -- Handle capability mode
    if self.active_capability then
        -- Get base header
        local capability_view_lines = self:render_header(false)
        -- Add breadcrumb
        local breadcrumb = NuiLine()
        breadcrumb
            :append(self.active_capability.server_name, Text.highlights.muted)
            :append(" > ", Text.highlights.muted)
            :append(self.active_capability.name, Text.highlights.info)
        table.insert(capability_view_lines, Text.pad_line(breadcrumb))
        table.insert(capability_view_lines, self:divider())
        -- Let capability render its content
        vim.list_extend(capability_view_lines, self.active_capability:render(#capability_view_lines))
        return capability_view_lines
    end

    -- Servers section
    vim.list_extend(lines, self:render_servers(#lines))

    -- Add spacing before workspaces section
    table.insert(lines, Text.empty_line())
    table.insert(lines, Text.empty_line())

    -- Workspaces section
    vim.list_extend(lines, self:render_workspaces(#lines))

    return lines
end

--- Render workspaces section using renderer
---@param line_offset number Current line number
---@return NuiLine[] Lines for workspaces section
function MainView:render_workspaces(line_offset)
    return renderer.render_workspaces_section(line_offset, self)
end
return MainView

---[[
--- Base view for MCPHub UI
--- Provides common view functionality and base for view inheritance
---]]
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local ns_id = vim.api.nvim_create_namespace("MCPHub")
local renderer = require("mcphub.utils.renderer")

local VIEW_TYPES = {
    ---@type MCPHub.UI.ViewName[]
    SETUP_INDEPENDENT = { "logs", "help", "config" },
}

---@class View
---@field ui MCPHub.UI Parent UI instance
---@field name string View name
---@field keymaps table<string, {action: function, desc: string}> View-specific keymaps
---@field active_keymaps string[] Currently active keymap keys
---@field cursor_pos number[]|nil Last known cursor position
---@field interactive_lines { line: number, type: string, context: any }[] List of interactive lines
---@field hover_ns number Namespace for highlighting
---@field cursor_highlight number|nil Extmark ID for current highlight
---@field cursor_group number|nil Cursor movement tracking group
local View = {}
View.__index = View

function View:new(ui, name)
    local instance = {
        ui = ui,
        name = name or "unknown",
        keymaps = {},
        active_keymaps = {},
        cursor_pos = nil,
        interactive_lines = {},
        hover_ns = vim.api.nvim_create_namespace("MCPHub" .. name .. "Hover"),
        cursor_highlight = nil,
        cursor_group = nil,
    }
    return setmetatable(instance, self)
end

--- Get initial cursor position for this view
function View:get_initial_cursor_position()
    -- By default, position after header's divider
    local lines = self:render_header()
    if #lines > 0 then
        return #lines
    end
    return 1
end

--- Track current cursor position
function View:track_cursor()
    if self.ui.window and vim.api.nvim_win_is_valid(self.ui.window) then
        self.cursor_pos = vim.api.nvim_win_get_cursor(0)
    end
end

--- Set cursor position with bounds checking
---@param pos number[]|nil Position to set [line, col] or nil for last tracked position
---@param opts? {restore_col: boolean} Options for cursor setting (default: {restore_col: true})
function View:set_cursor(pos, opts)
    -- Use provided position or last tracked position
    local cursor = pos or self.cursor_pos
    if not cursor then
        return
    end
    -- Ensure window is valid
    if not (self.ui.window and vim.api.nvim_win_is_valid(self.ui.window)) then
        return
    end
    -- Ensure line is within bounds
    local line_count = vim.api.nvim_buf_line_count(self.ui.buffer)
    local new_pos = { math.min(cursor[1], line_count), cursor[2] }
    -- Set cursor
    vim.api.nvim_win_set_cursor(self.ui.window, new_pos)
end

--- Register a view-specific keymap
---@param key string Key to map
---@param action function Action to perform
---@param desc string Description for which-key
---@param silent? boolean Whether to hide from footer (default: false)
function View:add_keymap(key, action, desc, silent)
    self.keymaps[key] = {
        action = action,
        desc = desc,
        silent = silent or false,
    }
end

--- Add a silent keymap alias that shares the same action as an existing keymap
---@param alias_key string New key to map
---@param existing_key string Existing key to copy action from
function View:add_keymap_alias(alias_key, existing_key)
    local existing = self.keymaps[existing_key]
    if existing then
        self.keymaps[alias_key] = {
            action = existing.action,
            desc = existing.desc,
            silent = true,
        }
    end
end

--- Apply all registered keymaps
function View:apply_keymaps()
    local buffer = self.ui.buffer
    self:clear_keymaps()

    -- Apply view's registered keymaps
    for key, map in pairs(self.keymaps) do
        vim.keymap.set("n", key, map.action, {
            buffer = buffer,
            desc = map.desc,
            nowait = true,
        })
        table.insert(self.active_keymaps, key)
    end
end

function View:clear_keymaps()
    for _, key in ipairs(self.active_keymaps) do
        pcall(vim.keymap.del, "n", key, {
            buffer = self.ui.buffer,
        })
    end
    self.active_keymaps = {} -- Clear the active keymaps array after deletion
end

--- Save cursor position before leaving
function View:save_cursor_position()
    if self.ui.window and vim.api.nvim_win_is_valid(self.ui.window) then
        self.ui.cursor_states[self.name] = vim.api.nvim_win_get_cursor(0)
    end
end

--- Restore cursor position after entering
function View:restore_cursor_position()
    if not (self.ui.window and vim.api.nvim_win_is_valid(self.ui.window)) then
        return
    end

    local saved_pos = self.ui.cursor_states[self.name]
    local line_count = vim.api.nvim_buf_line_count(self.ui.buffer)
    if saved_pos then
        -- Ensure position is valid
        local new_pos = { math.min(saved_pos[1], line_count), saved_pos[2] }
        vim.api.nvim_win_set_cursor(0, new_pos)
    else
        -- Use initial position if no saved position
        local initial_line = self:get_initial_cursor_position()
        if initial_line then
            local new_pos = { math.min(initial_line, line_count), 2 }
            vim.api.nvim_win_set_cursor(0, new_pos)
        end
    end
end

--- Called before view is drawn (override in child views)
function View:before_enter() end

--- Called after view is drawn and applied
function View:after_enter()
    -- Add cursor movement autocmd
    self.cursor_group = vim.api.nvim_create_augroup("MCPHub" .. self.name .. "Cursor", {
        clear = true,
    })
    vim.api.nvim_create_autocmd("CursorMoved", {
        group = self.cursor_group,
        buffer = self.ui.buffer,
        callback = function()
            self:handle_cursor_move()
        end,
    })

    self:apply_keymaps()
    self:restore_cursor_position()
end

--- Called before leaving view (override in child views)
function View:before_leave()
    self:save_cursor_position()
end

--- Called after leaving view
function View:after_leave()
    -- Clean up cursor tracking
    if self.cursor_group then
        vim.api.nvim_del_augroup_by_name("MCPHub" .. self.name .. "Cursor")
        self.cursor_group = nil
    end

    -- Clear highlight
    if self.cursor_highlight then
        vim.api.nvim_buf_del_extmark(self.ui.buffer, self.hover_ns, self.cursor_highlight)
        self.cursor_highlight = nil
    end

    self:clear_keymaps()
end

-- Line tracking functionality
function View:track_line(line_nr, type, context)
    table.insert(self.interactive_lines, {
        line = line_nr,
        type = type,
        context = context,
    })
end

function View:clear_line_tracking()
    self.interactive_lines = {}
end

---@param line_nr number
---@return string?, table?
function View:get_line_info(line_nr)
    for _, tracked in ipairs(self.interactive_lines) do
        if tracked.line == line_nr then
            return tracked.type, tracked.context
        end
    end
    return nil, nil
end

function View:handle_cursor_move()
    -- Clear previous highlight
    if self.cursor_highlight then
        vim.api.nvim_buf_del_extmark(self.ui.buffer, self.hover_ns, self.cursor_highlight)
        self.cursor_highlight = nil
    end

    -- Get current line
    local cursor = vim.api.nvim_win_get_cursor(0)
    local line = cursor[1]

    -- Get line info
    local type, context = self:get_line_info(line)
    if type then
        -- Add virtual text without line highlight
        self.cursor_highlight = vim.api.nvim_buf_set_extmark(self.ui.buffer, self.hover_ns, line - 1, 0, {
            virt_text = { { context and context.hint or "[<l> Interact]", Text.highlights.muted } },
            virt_text_pos = "eol",
        })
    end
end

--- Whether the view should show setup errors
---@return boolean
function View:should_show_setup_error()
    return not vim.tbl_contains(VIEW_TYPES.SETUP_INDEPENDENT, self.name)
end

--- Get window width for centering
function View:get_width()
    return vim.api.nvim_win_get_width(self.ui.window)
end

-- Add divider
function View:divider(is_full)
    return Text.divider(self:get_width(), is_full)
end

--- Create an empty line
function View:line()
    local line = NuiLine():append(string.rep(" ", self:get_width()))
    return line
end

function View:center(line, highlight)
    return Text.align_text(line, self:get_width(), "center", highlight)
end

--- Render header for view
--- @return NuiLine[] Header lines
function View:render_header(add_new_line)
    add_new_line = add_new_line == nil and true or add_new_line
    local lines = Text.render_header(self:get_width(), self.ui.current_view)
    table.insert(lines, self:divider())
    if add_new_line then
        table.insert(lines, self:line())
    end
    return lines
end

--- Render setup error state
---@param lines NuiLine[] Existing lines
---@return NuiLine[] Updated lines
function View:render_setup_error(lines)
    table.insert(lines, Text.pad_line(NuiLine():append("Setup Failed:", Text.highlights.error)))

    for _, err in ipairs(State:get_errors()) do
        vim.list_extend(lines, renderer.render_error(err))
        table.insert(lines, Text.empty_line())
    end

    return lines
end

--- Render progress state
---@param lines NuiLine[] Existing lines
---@return NuiLine[] Updated lines
function View:render_setup_progress(lines)
    -- Show progress message
    table.insert(lines, Text.align_text("Setting up MCPHub...", self:get_width(), "center", Text.highlights.info))
    return vim.list_extend(lines, renderer.render_server_entries(State.server_output.entries))
end

--- Render default setup state
--- @param lines NuiLine[] Existing lines
--- @return NuiLine[] Updated lines
function View:render_default_setup(lines)
    -- Title
    table.insert(lines, Text.align_text("Welcome to MCP Hub!", self:get_width(), "center", Text.highlights.title))
    table.insert(lines, Text.empty_line())

    -- Main description
    table.insert(
        lines,
        Text.align_text(
            "MCPHub is a MCP client for neovim that integrates Model Context Protocol servers into your Neovim workflow. ",
            self:get_width(),
            "center",
            Text.highlights.muted
        )
    )
    table.insert(
        lines,
        Text.align_text(
            "Start by configuring with one of the options below. ",
            self:get_width(),
            "center",
            Text.highlights.muted
        )
    )
    table.insert(lines, Text.empty_line())

    -- Configuration options header
    table.insert(lines, Text.pad_line(NuiLine():append("Configuration Options: ", Text.highlights.header)))
    table.insert(
        lines,
        Text.pad_line(
            NuiLine():append(
                "Visit https://ravitemer.github.io/mcphub.nvim/configuration.html for all available options",
                Text.highlights.muted
            )
        )
    )
    table.insert(lines, Text.empty_line())

    local option1_line =
        NuiLine():append("1. ", Text.highlights.info):append("Using vim.g.mcphub: ", Text.highlights.success)
    local option1_info = NuiLine():append("   ", Text.highlights.muted):append(
        "Set vim.g.mcphub with mcphub options as a table in your init.lua or init.vim",
        Text.highlights.muted
    )
    table.insert(lines, Text.pad_line(option1_line))
    table.insert(lines, Text.pad_line(option1_info))

    local vim_g_example = NuiLine():append("   ", Text.highlights.muted):append(
        'vim.g.mcphub = { port = 37373, config = "/abs/path/to/servers.json" , ... }',
        Text.highlights.code
    )
    table.insert(lines, Text.pad_line(vim_g_example))
    table.insert(lines, Text.empty_line())

    -- Option 2: Traditional setup
    local option2_line = NuiLine():append("2. ", Text.highlights.info):append("Using setup():", Text.highlights.success)
    local option2_info =
        NuiLine():append("   ", Text.highlights.muted):append("Call the mcphub.setup function ", Text.highlights.muted)
    table.insert(lines, Text.pad_line(option2_line))
    table.insert(lines, Text.pad_line(option2_info))

    local setup_example = NuiLine()
        :append("   ", Text.highlights.muted)
        :append('require("mcphub").setup({ port = 37373, ... })', Text.highlights.code)
    table.insert(lines, Text.pad_line(setup_example))
    table.insert(lines, Text.empty_line())

    return lines
end

--- Render footer with keymaps
--- @return NuiLine[] Lines for footer
function View:render_footer()
    local lines = {}

    -- Add padding and divider
    table.insert(lines, Text.empty_line())
    table.insert(lines, self:divider(true))

    -- Get all keymaps
    local key_items = {}

    -- Add view-specific keymaps first (only non-silent ones)
    for key, map in pairs(self.keymaps or {}) do
        if not map.silent then
            table.insert(key_items, {
                key = key,
                desc = map.desc,
            })
        end
    end

    table.insert(key_items, {
        key = "r",
        desc = "Refresh",
    })
    table.insert(key_items, {
        key = "R",
        desc = "Restart",
    })
    -- Add common close
    table.insert(key_items, {
        key = "q",
        desc = "Close",
    })

    -- Format in a single line
    local keys_line = NuiLine()
    for i, key in ipairs(key_items) do
        if i > 1 then
            keys_line:append("  ", Text.highlights.muted)
        end
        keys_line
            :append(" " .. key.key .. " ", Text.highlights.header_shortcut)
            :append(" ", Text.highlights.muted)
            :append(key.desc, Text.highlights.muted)
    end

    table.insert(lines, Text.pad_line(keys_line))

    return lines
end

--- Render view content
--- Should be overridden by child views
--- @return NuiLine[] Lines to render
function View:render()
    -- Get base header
    local lines = self:render_header()

    -- Handle special states
    if State.setup_state == "failed" then
        if self:should_show_setup_error() then
            return self:render_setup_error(lines)
        end
    elseif State.setup_state == "in_progress" then
        if self:should_show_setup_error() then
            return self:render_setup_progress(lines)
        end
    elseif State.setup_state == "not_started" then
        if self:should_show_setup_error() then
            return self:render_default_setup(lines)
        end
    end

    -- Views should override this to provide content
    table.insert(lines, Text.pad_line(NuiLine():append("No content implemented for this view", Text.highlights.muted)))

    return lines
end

function View:lock()
    vim.api.nvim_buf_set_option(self.ui.buffer, "modifiable", false)
end

function View:unlock()
    vim.api.nvim_buf_set_option(self.ui.buffer, "modifiable", true)
end

function View:open()
    return self.ui.window and vim.api.nvim_win_is_valid(self.ui.window)
end

--- Draw view content to buffer
function View:draw()
    if not self:open() then
        return
    end

    -- Track cursor position before drawing
    self:track_cursor()

    -- Get buffer
    local buf = self.ui.buffer

    -- Reset view state
    self:clear_line_tracking()
    if self.cursor_highlight then
        vim.api.nvim_buf_del_extmark(self.ui.buffer, self.hover_ns, self.cursor_highlight)
        self.cursor_highlight = nil
    end

    -- Make buffer modifiable
    vim.api.nvim_buf_set_option(buf, "modifiable", true)

    -- Clear buffer
    vim.api.nvim_buf_set_lines(buf, 0, -1, false, {})

    -- Get content and footer lines
    local lines = self:render()
    local footer_lines = self:render_footer()

    -- Calculate if we need padding
    local win_height = vim.api.nvim_win_get_height(self.ui.window)
    local content_height = #lines
    local total_needed = win_height - content_height - #footer_lines

    -- Add padding if needed
    if total_needed > 0 then
        for _ = 1, total_needed do
            table.insert(lines, Text.empty_line())
        end
    end

    -- Add footer at the end
    vim.list_extend(lines, footer_lines)

    -- Render each line with proper highlights
    local line_idx = 1
    for _, line in ipairs(lines) do
        if type(line) == "string" then
            -- Handle string lines with potential newlines
            for _, l in ipairs(Text.multiline(line)) do
                ---@diagnostic disable-next-line: undefined-field
                l:render(buf, ns_id, line_idx)
                line_idx = line_idx + 1
            end
        else
            ---@diagnostic disable-next-line: undefined-field
            line:render(buf, ns_id, line_idx)
            line_idx = line_idx + 1
        end
    end

    -- Make buffer unmodifiable
    vim.api.nvim_buf_set_option(buf, "wrap", true)
    vim.api.nvim_buf_set_option(buf, "modifiable", false)

    -- Restore cursor position after drawing
    self:set_cursor()
end

return View

local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local config_manager = require("mcphub.utils.config_manager")
local constants = require("mcphub.utils.constants")
local utils = require("mcphub.utils")

local M = {}

--- Get display name for a config source
---@param config_source string Path to config file
---@return string Display name for the source
function M.get_source_display_name(config_source)
    if not config_source then
        return "Unknown"
    end

    -- Get filename
    local filename = vim.fn.fnamemodify(config_source, ":t")
    return filename
end

--- Render servers grouped by config source
---@param servers table[] List of servers
---@param lines table[] Existing lines to append to
---@param current_line number Current line number
---@param view MainView View instance for tracking
---@return number New current line
function M.render_servers_grouped(servers, lines, current_line, view)
    local config_files = config_manager.get_active_config_files(true)
    -- Render each group
    for i, config_source in ipairs(config_files) do
        local group_servers = vim.tbl_filter(function(s)
            return s.config_source == config_source
        end, servers)
        current_line = M.render_server_group(config_source, group_servers, lines, current_line, view)
        if i < #config_files then
            table.insert(lines, Text.empty_line())
            current_line = current_line + 1
        end
    end
    -- Show empty project group if no marker files are found
    if #config_files == 1 and State.config.workspace.enabled then
        table.insert(lines, Text.empty_line())
        current_line = current_line + 1
        local is_global = false
        local icon = is_global and Text.icons.globe or Text.icons.folder
        local prefix = icon .. " " .. (is_global and "Global" or "Project") .. " "
        local header_line = NuiLine():append(prefix, Text.highlights.title)
        table.insert(lines, Text.pad_line(header_line))
        current_line = current_line + 1
        local line = NuiLine()
        local look_for = table.concat(State.config.workspace.look_for or {}, ", ")
        line:append(string.format("%s not in path", look_for), Text.highlights.muted)
        table.insert(lines, Text.pad_line(line, nil, 4))
        current_line = current_line + 1
    end

    return current_line
end

--- Render a single server group with header
---@param config_source string Config file path
---@param servers table[] Servers in this group
---@param lines table[] Lines to append to
---@param current_line number Current line number
---@param view MainView View instance
---@return number New current line
function M.render_server_group(config_source, servers, lines, current_line, view)
    -- Sort servers within group (connected > disconnected > disabled)
    local sorted = vim.deepcopy(servers)
    table.sort(sorted, function(a, b)
        -- First compare status priority
        local status_priority = {
            connected = 1,
            disconnected = 2,
            disabled = 3,
        }
        local a_priority = status_priority[a.status] or 2
        local b_priority = status_priority[b.status] or 2

        if a_priority ~= b_priority then
            return a_priority < b_priority
        end

        -- If same status, sort alphabetically
        return a.name < b.name
    end)

    local is_global = config_source == State.config.config
    local icon = is_global and Text.icons.globe or Text.icons.folder
    local prefix = icon .. " " .. (is_global and "Global" or "Project") .. " "

    -- Render group header
    -- local display_name = M.get_source_display_name(config_source)
    local header_line = NuiLine():append(prefix, Text.highlights.title)
    table.insert(lines, Text.pad_line(header_line))
    current_line = current_line + 1
    if #sorted == 0 then
        -- No servers in this group
        table.insert(
            lines,
            Text.pad_line(
                NuiLine():append(
                    string.format("No servers found in `%s` (Install from Marketplace)", config_source),
                    Text.highlights.muted
                ),
                nil,
                4
            )
        )
        return current_line + 1
    end

    for _, server in ipairs(sorted) do
        local server_config = config_manager.get_server_config(server) or {}
        current_line = M.render_server_capabilities(server, lines, current_line, server_config, view, server.is_native)
    end

    return current_line
end

function M.get_hub_info(state)
    local icon = ({
        [constants.HubState.STARTING] = "◉ ",
        [constants.HubState.READY] = Text.icons.loaded .. " ",
        [constants.HubState.RESTARTING] = "◉ ",
        [constants.HubState.RESTARTED] = Text.icons.loaded .. " ",
        [constants.HubState.STOPPING] = "◉ ",
        [constants.HubState.STOPPED] = "○ ",
        [constants.HubState.ERROR] = Text.icons.error .. " ",
    })[state] or "⚠ "

    local desc = ({
        [constants.HubState.STARTING] = "Starting...",
        [constants.HubState.READY] = "Connected",
        [constants.HubState.RESTARTING] = "Restarting...",
        [constants.HubState.RESTARTED] = "Restarted",
        [constants.HubState.STOPPING] = "Stopping...",
        [constants.HubState.STOPPED] = "Stopped",
        [constants.HubState.ERROR] = "Error",
    })[state] or "Unknown"

    local hl = ({
        [constants.HubState.STARTING] = Text.highlights.success,
        [constants.HubState.READY] = Text.highlights.success,
        [constants.HubState.RESTARTING] = Text.highlights.success,
        [constants.HubState.RESTARTED] = Text.highlights.success,
        [constants.HubState.STOPPING] = Text.highlights.warn,
        [constants.HubState.STOPPED] = Text.highlights.warn,
        [constants.HubState.ERROR] = Text.highlights.error,
    })[state] or Text.highlights.error
    return {
        icon = icon,
        desc = desc,
        hl = hl,
    }
end

--- Get server status information
---@param status string Server status
---@return { icon: string, desc: string, hl: string } Status info
function M.get_server_status_info(status, expanded)
    return {
        icon = ({
            connected = (expanded and Text.icons.triangleDown or Text.icons.triangleRight) .. " ",
            connecting = "◉ ",
            restarting = "◉ ",
            unauthorized = Text.icons.unauthorized .. " ",
            disconnecting = "○ ",
            disconnected = "○ ",
            disabled = "○ ",
        })[status] or "⚠ ",

        desc = ({
            connecting = " (connecting...)",
            disconnecting = " (disconnecting...)",
            unauthorized = " (unauthorized)",
        })[status] or "",

        hl = ({
            connected = Text.highlights.success,
            connecting = Text.highlights.success,
            restarting = Text.highlights.success,
            disconnecting = Text.highlights.warn,
            disconnected = Text.highlights.warn,
            unauthorized = Text.highlights.warn,
            disabled = Text.highlights.muted,
        })[status] or Text.highlights.error,
    }
end

--- Render server capabilities section
---@param items table[] List of items
---@param title string Section title
---@param server_name string Server name
---@param line_type string Item type
---@param current_line number Current line number
---@param server_config table Server configuration
---@return NuiLine[],number,table
function M.render_cap_section(items, title, server_name, line_type, current_line, server_config)
    local lines = {}
    local mappings = {}

    local icons = {
        tool = Text.icons.tool,
        resource = Text.icons.resource,
        resourceTemplate = Text.icons.resourceTemplate,
        prompt = Text.icons.prompt,
    }
    table.insert(
        lines,
        Text.pad_line(NuiLine():append(" " .. icons[line_type] .. " " .. title .. ": ", Text.highlights.muted), nil, 4)
    )

    local disabled_tools = server_config.disabled_tools or {}
    if line_type == "tool" then
        -- For tools, sort by name and move disabled ones to end
        local sorted_items = vim.deepcopy(items)
        table.sort(sorted_items, function(a, b)
            local a_disabled = vim.tbl_contains(disabled_tools, a.name)
            local b_disabled = vim.tbl_contains(disabled_tools, b.name)
            if a_disabled ~= b_disabled then
                return not a_disabled
            end
            return a.name < b.name
        end)
        items = sorted_items
    end

    for _, item in ipairs(items) do
        local name = item.name or item.uri or item.uriTemplate or "NO NAME"
        local is_disabled = false
        if line_type == "tool" then
            is_disabled = vim.tbl_contains(disabled_tools, item.name)
        elseif line_type == "resource" then
            is_disabled = vim.tbl_contains(server_config.disabled_resources or {}, item.uri)
        elseif line_type == "resourceTemplate" then
            is_disabled = vim.tbl_contains(server_config.disabled_resourceTemplates or {}, item.uriTemplate)
        elseif line_type == "prompt" then
            is_disabled = vim.tbl_contains(server_config.disabled_prompts or {}, item.name)
        end

        local line = NuiLine()
        if is_disabled then
            line:append(Text.icons.circle .. " ", Text.highlights.muted):append(name, Text.highlights.muted)
        else
            line:append(Text.icons.arrowRight .. " ", Text.highlights.muted):append(name, Text.highlights.info)
        end

        if item.mimeType then
            line:append(" (" .. item.mimeType .. ")", Text.highlights.muted)
        end

        -- Show auto-approve status for tools
        if line_type == "tool" and not is_disabled then
            local auto_approve = server_config.autoApprove
            local tool_auto_approved = false

            if auto_approve == true then
                tool_auto_approved = true
            elseif type(auto_approve) == "table" and vim.islist(auto_approve) then
                tool_auto_approved = vim.tbl_contains(auto_approve, name)
            end

            if tool_auto_approved then
                line:append(" " .. Text.icons.auto, Text.highlights.success)
            end
        end

        table.insert(lines, Text.pad_line(line, nil, 6))

        local hint
        if is_disabled then
            hint = "[<t> Toggle]"
        elseif line_type == "tool" then
            hint = string.format("[<l> open %s, <t> Toggle, <a> Auto-approve]", line_type)
        else
            hint = string.format("[<l> open %s, <t> Toggle]", line_type)
        end
        table.insert(mappings, {
            line = current_line + #lines,
            type = line_type,
            context = {
                def = item,
                server_name = server_name,
                disabled = is_disabled,
                hint = hint,
            },
        })
    end

    return lines, current_line + #lines, mappings
end

--- Function to render a single server's capabilities
---@param server table Server to render
---@param lines table[] Lines array to append to
---@param current_line number Current line number
---@param server_config table Config source for the server
---@param view MainView View instance for tracking
---@return number New current line
function M.render_server_capabilities(server, lines, current_line, server_config, view, is_native)
    local server_name_line = M.render_server_line(server, view.expanded_server == server.name)
    table.insert(lines, Text.pad_line(server_name_line, nil, 3))
    current_line = current_line + 1

    -- Prepare hover hint based on server status
    local base_hint_disabled = is_native and "[<t> Toggle, <e> Edit]" or "[<t> Toggle, <e> Edit, <d> Delete]"
    local base_hint_enabled = is_native and "[<t> Toggle, <a> Auto-approve, <e> Edit]"
        or "[<t> Toggle, <a> Auto-approve, <e> Edit, <d> Delete]"

    local needs_authorization = server.status == "unauthorized"
    local enabled_hint = is_native and "[<l> Expand, <t> Toggle, <a> Auto-approve, <e> Edit]"
        or needs_authorization and "[<l> Authorize, <t> Toggle, <a> Auto-approve, <e> Edit, <d> Delete]"
        or "[<l> Expand, <t> Toggle, <a> Auto-approve, <e> Edit, <d> Delete]"
    local expanded_hint = is_native and "[<h> Collapse, <t> Toggle, <a> Auto-approve, <e> Edit]"
        or "[<h> Collapse, <t> Toggle, <a> Auto-approve, <e> Edit, <d> Delete]"

    local hint
    if server.status == "disabled" or server.status == "disconnected" then
        hint = base_hint_disabled -- No auto-approve for disabled/disconnected servers
    else
        hint = view.expanded_server == server.name and expanded_hint or enabled_hint
    end

    view:track_line(current_line, "server", {
        name = server.name,
        status = server.status,
        hint = hint,
    })

    -- Show expanded server capabilities
    if server.status == "connected" and server.capabilities and view.expanded_server == server.name then
        -- local desc = server.description or ""
        -- if desc ~= "" then
        --     table.insert(lines, Text.pad_line(NuiLine():append(tostring(desc), Text.highlights.muted), nil, 5))
        --     current_line = current_line + 1
        -- end

        if
            #server.capabilities.tools + #server.capabilities.resources + #server.capabilities.resourceTemplates
            == 0
        then
            table.insert(
                lines,
                Text.pad_line(NuiLine():append("No capabilities available", Text.highlights.muted), nil, 6)
            )
            table.insert(lines, Text.empty_line())
            current_line = current_line + 2
            return current_line
        end

        local custom_instructions = server_config.custom_instructions or {}
        local is_disabled = custom_instructions.disabled == true
        local has_instructions = custom_instructions.text and #custom_instructions.text > 0
        local ci_line = NuiLine()
            :append(is_disabled and Text.icons.circle or Text.icons.instructions, Text.highlights.muted)
            :append(
                " Custom Instructions" .. (not is_disabled and not has_instructions and " (empty)" or ""),
                (is_disabled or not has_instructions) and Text.highlights.muted or Text.highlights.info
            )
        table.insert(lines, Text.pad_line(ci_line, nil, 5))
        current_line = current_line + 1
        view:track_line(current_line, "customInstructions", {
            server_name = server.name,
            disabled = is_disabled,
            name = Text.icons.instructions .. " Custom Instructions",
            hint = is_disabled and "[<t> Toggle]" or "[<e> Edit, <t> Toggle]",
        })
        table.insert(lines, Text.empty_line())
        current_line = current_line + 1

        if #server.capabilities.prompts > 0 then
            local section_lines, new_line, mappings = M.render_cap_section(
                server.capabilities.prompts,
                "Prompts",
                server.name,
                "prompt",
                current_line,
                server_config
            )
            vim.list_extend(lines, section_lines)
            for _, m in ipairs(mappings) do
                view:track_line(m.line, m.type, m.context)
            end
            table.insert(lines, Text.empty_line())
            current_line = new_line + 1
        end
        -- Tools section if any
        if #server.capabilities.tools > 0 then
            local section_lines, new_line, mappings = M.render_cap_section(
                server.capabilities.tools,
                "Tools",
                server.name,
                "tool",
                current_line,
                server_config
            )
            vim.list_extend(lines, section_lines)
            for _, m in ipairs(mappings) do
                view:track_line(m.line, m.type, m.context)
            end
            table.insert(lines, Text.empty_line())
            current_line = new_line + 1
        end

        -- Resources section if any
        if #server.capabilities.resources > 0 then
            local section_lines, new_line, mappings = M.render_cap_section(
                server.capabilities.resources,
                "Resources",
                server.name,
                "resource",
                current_line,
                server_config
            )
            vim.list_extend(lines, section_lines)
            for _, m in ipairs(mappings) do
                view:track_line(m.line, m.type, m.context)
            end
            table.insert(lines, Text.empty_line())
            current_line = new_line + 1
        end

        -- Resource Templates section if any
        if #server.capabilities.resourceTemplates > 0 then
            local section_lines, new_line, mappings = M.render_cap_section(
                server.capabilities.resourceTemplates,
                "Resource Templates",
                server.name,
                "resourceTemplate",
                current_line,
                server_config
            )
            vim.list_extend(lines, section_lines)
            for _, m in ipairs(mappings) do
                view:track_line(m.line, m.type, m.context)
            end
            table.insert(lines, Text.empty_line())
            current_line = new_line + 1
        end
    end

    return current_line
end

--- Render a server line
---@param server table Server data
---@return { line: NuiLine, mapping: table? }
function M.render_server_line(server, active)
    local server_config = config_manager.get_server_config(server.name) or {}
    local is_enabled = server_config.disabled ~= true
    if server.is_native and not is_enabled then
        server.status = "disabled"
    end
    local status = M.get_server_status_info(server.status, active)
    local line = NuiLine()
    local hl = server.status == "connected" and Text.highlights.success or status.hl
    line:append(status.icon, status.hl)
    line:append(server.displayName or server.name, hl)
    if server.transportType == "sse" then
        line:append(" " .. (server.status == "connected" and Text.icons.sse or Text.icons.sse), hl)
    else
    end

    --INFO: when decoded from regualr mcp servers vim.NIL; for nativeservers we set nil, so check both
    -- Add error message for disconnected servers
    if server.error ~= vim.NIL and server.error ~= nil and server.status == "disconnected" and server.error ~= "" then
        -- Get first line of error message
        local error_lines = Text.multiline(server.error, Text.highlights.error)
        line:append(" - ", Text.highlights.muted):append(error_lines[1], Text.highlights.error)
    end

    -- Add capabilities counts inline for connected servers
    if server.status == "connected" and server.capabilities then
        local custom_instructions = server_config.custom_instructions
        if custom_instructions and custom_instructions.text and custom_instructions.text ~= "" then
            local is_disabled = server_config.custom_instructions.disabled
            line:append(
                " " .. Text.icons.instructions .. " ",
                is_disabled and Text.highlights.muted or Text.highlights.success
            )
        end
        -- Show auto-approve status for server using smart detection
        local all_tools = {}
        for _, tool in ipairs(server.capabilities.tools or {}) do
            table.insert(all_tools, tool.name)
        end

        local auto_approve = server_config.autoApprove
        local status = "none"
        local count = 0

        if auto_approve == true then
            status = "all"
            count = #all_tools
        elseif type(auto_approve) == "table" and vim.islist(auto_approve) then
            if #auto_approve == 0 then
                status = "none"
                count = 0
            elseif #auto_approve == #all_tools and #all_tools > 0 then
                status = "all"
                count = #all_tools
            else
                status = "partial"
                count = #auto_approve
            end
        end

        if status == "all" then
            -- All tools auto-approved
            line:append(" " .. Text.icons.auto .. " ", Text.highlights.success)
        elseif status == "partial" then
            -- Partial auto-approval (show count)
            line:append(" " .. Text.icons.auto .. " " .. tostring(count) .. " ", Text.highlights.warn)
        end

        -- Helper to render capability count with active/total
        local function render_capability_count(capabilities, disabled_list, id_field, icon, highlight)
            if capabilities and #capabilities > 0 then
                local current_ids = vim.tbl_map(function(cap)
                    return cap[id_field]
                end, capabilities)
                local disabled = vim.tbl_filter(function(item)
                    return vim.tbl_contains(current_ids, item)
                end, disabled_list or {})
                local enabled = #capabilities - #disabled

                line:append(icon, highlight):append(
                    (" " .. tostring(enabled) .. (#disabled > 0 and "/" .. tostring(#capabilities) or "")),
                    Text.highlights.muted
                )
                return true
            end
        end
        -- line:append(" [", Text.highlights.muted)

        -- if
        --     render_capability_count(
        --         server.capabilities.prompts,
        --         server_config.disabled_prompts,
        --         "name",
        --         Text.icons.prompt,
        --         Text.highlights.muted
        --     )
        -- then
        --     line:append(" ")
        -- end
        -- render_capability_count(
        --     server.capabilities.tools,
        --     server_config.disabled_tools,
        --     "name",
        --     Text.icons.tool,
        --     Text.highlights.success
        -- )
        -- line:append(" ")
        -- render_capability_count(
        --     server.capabilities.resources,
        --     server_config.disabled_resources,
        --     "uri",
        --     Text.icons.resource,
        --     Text.highlights.warning
        -- )
        -- line:append(" ")
        -- render_capability_count(
        --     server.capabilities.resourceTemplates,
        --     server_config.disabled_resourceTemplates,
        --     "uriTemplate",
        --     Text.icons.resourceTemplate,
        --     Text.highlights.error
        -- )
        -- line:append("]", Text.highlights.muted)
    end

    -- Add status description if any
    if status.desc ~= "" then
        line:append(status.desc, Text.highlights.muted)
    end

    return line
end

-- Format timestamp (could be Unix timestamp or ISO string)
local function format_time(timestamp)
    -- For Unix timestamps
    return os.date("%H:%M:%S", math.floor(timestamp / 1000))
end

--- Render error lines without header
---@param error_type? string Optional error type to filter (setup/server/runtime)
---@param detailed? boolean Whether to show full error details
---@return NuiLine[] Lines
function M.render_hub_errors(error_type, detailed)
    local lines = {}
    local errors = State:get_errors(error_type)
    if #errors > 0 then
        for _, err in ipairs(errors) do
            vim.list_extend(lines, M.render_error(err, detailed))
        end
    end
    return lines
end

--- Render server entry logs without header
---@param entries table[] Array of log entries
---@return NuiLine[] Lines
function M.render_server_entries(entries)
    local lines = {}

    if #entries > 0 then
        for _, entry in ipairs(entries) do
            if entry.timestamp and entry.message then
                local line = NuiLine()
                -- Add timestamp
                line:append(string.format("[%s] ", format_time(entry.timestamp)), Text.highlights.muted)

                -- Add type icon and message
                line:append(
                    (Text.icons[entry.type] or "•") .. " ",
                    Text.highlights[entry.type] or Text.highlights.muted
                )

                -- Add error code if present
                if entry.code then
                    line:append(string.format("[Code: %s] ", entry.code), Text.highlights.muted)
                end

                -- Add main message
                line:append(entry.message, Text.highlights[entry.type] or Text.highlights.muted)

                table.insert(lines, Text.pad_line(line))
            end
        end
    end

    return lines
end

function M.render_error(err, detailed)
    local lines = {}
    -- Get appropriate icon based on error type
    local error_icon = ({
        SETUP = Text.icons.setup_error,
        SERVER = Text.icons.server_error,
        RUNTIME = Text.icons.runtime_error,
    })[err.type] or Text.icons.error

    -- Handle multiline error messages
    local message_lines = Text.multiline(err.message, Text.highlights.error)

    -- First line with icon and timestamp
    local first_line = NuiLine()
    first_line:append(error_icon .. " ", Text.highlights.error)
    first_line:append(message_lines[1], Text.highlights.error)
    if err.timestamp then
        first_line:append(" (" .. utils.format_relative_time(err.timestamp) .. ")", Text.highlights.muted)
    end
    table.insert(lines, Text.pad_line(first_line))

    -- Add remaining lines with proper indentation
    for i = 2, #message_lines do
        local line = NuiLine()
        line:append(message_lines[i], Text.highlights.error)
        table.insert(lines, Text.pad_line(line, nil, 4))
    end

    -- Add error details if detailed mode and details exist
    if detailed and err.details and next(err.details) then
        -- Convert details to string
        local detail_text = type(err.details) == "string" and err.details or vim.inspect(err.details)

        -- Add indented details
        local detail_lines = vim.tbl_map(function(l)
            return Text.pad_line(l, nil, 4)
        end, Text.multiline(detail_text, Text.highlights.muted))
        vim.list_extend(lines, detail_lines)
        table.insert(lines, Text.empty_line())
    end
    return lines
end

--- Get workspace state highlight based on current state
---@param workspace_details table Workspace details from cache
---@return string Highlight group name
local function get_workspace_highlight(workspace_details)
    if workspace_details.state == "shutting_down" then
        return Text.highlights.warn
    elseif workspace_details.state == "active" then
        return Text.highlights.success
    else
        return Text.highlights.error
    end
end

--- Get workspace time display (uptime or countdown)
---@param workspace_details table Workspace details
---@return string, string Time text and icon
local function get_workspace_time_info(workspace_details)
    if not workspace_details then
        return "unknown", Text.icons.clock
    end
    if
        workspace_details.state == "shutting_down"
        and workspace_details.shutdownStartedAt
        and workspace_details.shutdownDelay
    then
        -- Calculate remaining time
        local start_time = workspace_details.shutdownStartedAt
        local delay_ms = workspace_details.shutdownDelay
        local current_time = os.time() * 1000 -- Convert to milliseconds

        -- Parse ISO time to milliseconds
        local start_ms = 0
        if start_time then
            local year, month, day, hour, min, sec = start_time:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
            if year then
                local ok, secs = pcall(os.time, {
                    ---@diagnostic disable-next-line: assign-type-mismatch
                    year = tonumber(year),
                    ---@diagnostic disable-next-line: assign-type-mismatch
                    month = tonumber(month),
                    ---@diagnostic disable-next-line: assign-type-mismatch
                    day = tonumber(day),
                    hour = tonumber(hour),
                    min = tonumber(min),
                    sec = tonumber(sec),
                })
                if ok then
                    start_ms = secs * 1000 -- Convert to milliseconds
                else
                    -- Fallback to current time if parsing fails
                    start_ms = current_time
                end
            end
        end

        local elapsed = current_time - start_ms
        local remaining = math.max(0, delay_ms - elapsed)

        if remaining > 0 then
            local remaining_seconds = math.floor(remaining / 1000)
            local minutes = math.floor(remaining_seconds / 60)
            local seconds = remaining_seconds % 60
            return string.format("shutdown in %dm %ds", minutes, seconds), Text.icons.hourglass
        else
            return "closing...", Text.icons.hourglass
        end
    else
        -- Show uptime
        if workspace_details.startTime then
            local uptime = utils.iso_to_relative_time(workspace_details.startTime) .. " ago"
            return uptime, Text.icons.clock
        end
        return "unknown", Text.icons.clock
    end
end

--- Render workspace line (collapsed view)
---@param workspace_details MCPHub.WorkspaceDetails Workspace details from cache
---@param port_str string Port as string (cache key)
---@param is_current boolean Whether this is the current workspace
---@param is_expanded boolean Whether this workspace is expanded
---@return NuiLine
function M.render_workspace_line(workspace_details, port_str, is_current, is_expanded)
    local line = NuiLine()
    local hl = get_workspace_highlight(workspace_details)

    -- Expansion indicator
    local expand_icon = is_expanded and Text.icons.triangleDown or Text.icons.triangleRight
    line:append(expand_icon .. " ", hl)

    -- Folder icon and name
    local workspace_name = vim.fn.fnamemodify(workspace_details.cwd, ":t") -- Get directory name
    if tostring(workspace_details.port) == tostring(State.config.port) then
        workspace_name = "Global"
    end
    line:append(Text.icons.folder .. " " .. workspace_name, hl)

    -- Current indicator
    if is_current then
        line:append(" (current)", Text.highlights.success)
    end

    -- Port
    line:append(" | " .. Text.icons.tower .. " " .. port_str, Text.highlights.muted)

    -- Client count
    local client_count = workspace_details.activeConnections or 0
    if client_count > 0 then
        line:append(" | " .. Text.icons.person .. " " .. tostring(client_count), Text.highlights.muted)
    end

    -- Time info
    local time_text, time_icon = get_workspace_time_info(workspace_details)
    line:append(" | " .. time_icon .. " " .. time_text, Text.highlights.muted)

    return line
end

--- Render workspace details (expanded view)
---@param workspace_details MCPHub.WorkspaceDetails Workspace details from cache
---@param current_line number Current line number
---@return NuiLine[], number, table[] Lines new current line and line mappings
function M.render_workspace_details(workspace_details, current_line)
    local lines = {}
    local mappings = {}

    -- Config files section
    if workspace_details.config_files and #workspace_details.config_files > 0 then
        table.insert(lines, Text.empty_line())
        current_line = current_line + 1

        table.insert(lines, Text.pad_line(NuiLine():append("Config Files Used:", Text.highlights.muted), nil, 5))
        current_line = current_line + 1

        for _, config_file in ipairs(workspace_details.config_files) do
            local config_line = NuiLine()
            config_line:append(Text.icons.file .. " ", Text.highlights.muted)

            config_line:append(config_file .. " ", Text.highlights.info)

            table.insert(lines, Text.pad_line(config_line, nil, 6))
            current_line = current_line + 1
        end
    end

    -- Empty line for spacing
    table.insert(lines, Text.empty_line())
    current_line = current_line + 1

    -- Details section
    table.insert(lines, Text.pad_line(NuiLine():append("Details:", Text.highlights.muted), nil, 5))
    current_line = current_line + 1

    -- Time info
    local time_text = get_workspace_time_info(workspace_details)
    -- State information
    local state_line = NuiLine()
    state_line:append("State: ", Text.highlights.muted)
    local state_text = workspace_details.state == "shutting_down" and ("Hub will " .. time_text) or "Active"
    local state_hl = workspace_details.state == "shutting_down" and Text.highlights.warn or Text.highlights.success
    state_line:append(state_text, state_hl)
    table.insert(lines, Text.pad_line(state_line, nil, 6))
    current_line = current_line + 1

    -- Clients connected
    if workspace_details.activeConnections then
        local clients_line = NuiLine()
        clients_line:append("Connected Clients: ", Text.highlights.muted)
        clients_line:append(tostring(workspace_details.activeConnections), Text.highlights.info)
        table.insert(lines, Text.pad_line(clients_line, nil, 6))
        current_line = current_line + 1
    end
    -- Path
    local path_line = NuiLine()
    path_line:append("CWD: ", Text.highlights.muted)
    path_line:append(workspace_details.cwd, Text.highlights.info)
    table.insert(lines, Text.pad_line(path_line, nil, 6))
    current_line = current_line + 1

    -- Process ID
    local pid_line = NuiLine()
    pid_line:append("Process ID: ", Text.highlights.muted)
    pid_line:append(tostring(workspace_details.pid), Text.highlights.info)
    table.insert(lines, Text.pad_line(pid_line, nil, 6))
    current_line = current_line + 1

    -- Started time
    if workspace_details.startTime then
        local started_line = NuiLine()
        started_line:append("Started: ", Text.highlights.muted)
        -- Format the ISO time nicely
        local start_time = workspace_details.startTime
        local formatted_time = utils.iso_to_relative_time(start_time) .. " ago"
        started_line:append(formatted_time, Text.highlights.info)
        table.insert(lines, Text.pad_line(started_line, nil, 6))
        current_line = current_line + 1
    end

    if workspace_details.shutdownDelay then
        local shutdown_line = NuiLine()
        shutdown_line:append("shutdown-delay: ", Text.highlights.muted)
        local formatted_time = utils.ms_to_relative_time(workspace_details.shutdownDelay)
        shutdown_line:append(formatted_time, Text.highlights.info)
        shutdown_line:append(
            string.format(
                " (When 0 clients connected, will wait for %s for any new connections before shutting down)",
                formatted_time
            ),
            Text.highlights.muted
        )
        table.insert(lines, Text.pad_line(shutdown_line, nil, 6))
        current_line = current_line + 1
    end
    table.insert(lines, Text.empty_line())
    current_line = current_line + 1

    return lines, current_line, mappings
end

--- Render all workspaces section
---@param line_offset number Current line number
---@param view MainView View instance for tracking interactions
---@return NuiLine[] Lines for workspaces section
function M.render_workspaces_section(line_offset, view)
    local lines = {}
    local current_line = line_offset

    -- Section header
    table.insert(lines, Text.pad_line(NuiLine():append(Text.icons.server .. " Active Hubs", Text.highlights.title)))
    current_line = current_line + 1

    local workspaces = State.server_state.workspaces
    if not workspaces or not workspaces.allActive or vim.tbl_isempty(workspaces.allActive) then
        table.insert(lines, Text.pad_line(NuiLine():append("No active workspaces", Text.highlights.muted)))
        current_line = current_line + 1
        return lines
    end

    local current_port = workspaces.current

    -- Sort workspaces: current first, then by port number
    local ports = vim.tbl_keys(workspaces.allActive)
    table.sort(ports, function(a, b)
        if a == current_port then
            return true
        elseif b == current_port then
            return false
        else
            return (tonumber(a) or 0) < (tonumber(b) or 0)
        end
    end)

    for _, port_str in ipairs(ports) do
        local workspace_details = workspaces.allActive[port_str]
        local is_current = port_str == current_port
        local is_expanded = view.expanded_workspace == port_str

        -- Render workspace line
        local workspace_line = M.render_workspace_line(workspace_details, port_str, is_current, is_expanded)
        table.insert(lines, Text.pad_line(workspace_line, nil, 3))
        current_line = current_line + 1

        -- Track line for interaction
        local base_hint = is_expanded and "[<h> Collapse" or "[<l> Expand"
        local action_hint = ", <d> Kill"
        local change_dir_hint = is_current and "" or ", <gc> Change Dir"
        local full_hint = base_hint .. action_hint .. change_dir_hint .. "]"

        view:track_line(current_line, "workspace", {
            port = port_str,
            workspace_details = workspace_details,
            is_current = is_current,
            hint = full_hint,
        })

        -- Render expanded details if this workspace is expanded
        if is_expanded then
            local detail_lines, new_line, mappings = M.render_workspace_details(workspace_details, current_line)
            vim.list_extend(lines, detail_lines)
            current_line = new_line

            -- Track any interactive lines from details (for future actions)
            for _, mapping in ipairs(mappings) do
                view:track_line(mapping.line, mapping.type, mapping.context)
            end
        end
    end

    return lines
end

return M

local M = {}
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local async = require("plenary.async")
local constants = require("mcphub.utils.constants")

---@param title string Title of the floating window
---@param content string Content to be displayed in the floating window
---@param on_save fun(new_content:string) Callback function to be called when the user saves the content
---@param opts {filetype?: string, validate?: function, show_footer?: boolean, start_insert?: boolean, on_cancel?: function, position?: "cursor"|"center", go_to_placeholder?: boolean, virtual_lines?: Array[]} Options for the floating window
function M.multiline_input(title, content, on_save, opts)
    opts = opts or {}
    local bufnr = vim.api.nvim_create_buf(false, true)
    local cursor = vim.api.nvim_win_get_cursor(0)
    local width = vim.api.nvim_win_get_width(0)
    local max_width = 70
    width = math.min(width, max_width) - 3

    -- Set buffer options
    vim.api.nvim_buf_set_option(bufnr, "buftype", "acwrite")
    vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
    vim.api.nvim_buf_set_option(bufnr, "swapfile", false)
    if opts.filetype then
        vim.api.nvim_buf_set_option(bufnr, "filetype", opts.filetype)
    else
        vim.api.nvim_buf_set_option(bufnr, "filetype", "text")
    end

    local lines = vim.split(content or "", "\n")

    -- Set initial content
    vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
    local height = 8
    local auto_height = #lines + #(opts.virtual_lines or {}) + 1
    if auto_height > height then
        height = math.min(auto_height, vim.api.nvim_win_get_height(0) - 2)
    end

    local win_opts = {
        relative = "win",
        bufpos = cursor,
        width = width,
        focusable = true,
        height = height,
        anchor = "NW",
        style = "minimal",
        border = "rounded",
        title = { { " " .. title .. " ", Text.highlights.title } },
        title_pos = "center",
        footer = opts.show_footer ~= false and {
            { " ", nil },
            { " <Cr> ", Text.highlights.title },
            { ": Submit | ", Text.highlights.muted },
            { " <Esc> ", Text.highlights.title },
            { ",", Text.highlights.muted },
            { " q ", Text.highlights.title },
            { ": Cancel ", Text.highlights.muted },
        } or "",
    }

    -- Override positioning if center is requested
    if opts.position == "center" then
        win_opts.relative = "editor"
        win_opts.bufpos = nil
        win_opts.row = 1
        win_opts.col = math.floor((vim.o.columns - width) / 2)
    end

    -- Create floating window
    local win = vim.api.nvim_open_win(bufnr, true, win_opts)

    -- Set window options
    vim.api.nvim_win_set_option(win, "number", false)
    vim.api.nvim_win_set_option(win, "relativenumber", false)
    vim.api.nvim_win_set_option(win, "wrap", false)
    vim.api.nvim_win_set_option(win, "cursorline", false)

    -- Create namespace for virtual text
    local ns = vim.api.nvim_create_namespace("MCPHubMultiLineInput")

    -- Function to update virtual text at cursor position
    local function update_virtual_text()
        vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
        if vim.fn.mode() == "n" then
            local cursor = vim.api.nvim_win_get_cursor(0)
            local row = cursor[1] - 1
            vim.api.nvim_buf_set_extmark(bufnr, ns, row, 0, {
                virt_text = { { "[<i> Edit, <Cr> Save]", "Comment" } },
                virt_text_pos = "eol",
            })
        end
    end

    -- Set up autocmd for cursor movement and mode changes
    local group = vim.api.nvim_create_augroup("MCPHubMultiLineInputCursor", { clear = true })
    vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI", "ModeChanged" }, {
        buffer = bufnr,
        group = group,
        callback = update_virtual_text,
    })

    -- Set buffer local mappings
    local function save_and_close()
        local new_content = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n")
        new_content = vim.trim(new_content)
        if opts.validate then
            local valid = opts.validate(new_content)
            if not valid then
                return
            end
        end
        -- Close the window
        vim.api.nvim_win_close(win, true)
        -- -- Call save callback if content changed
        -- if content ~= new_content then
        on_save(new_content)
        -- end
    end

    local function close_window()
        vim.api.nvim_win_close(win, true)
        if opts.on_cancel then
            opts.on_cancel()
        end
    end

    -- Add mappings for normal mode
    local mappings = {
        ["<CR>"] = save_and_close,
        ["<Esc>"] = close_window,
        ["q"] = close_window,
    }
    -- Apply mappings
    for key, action in pairs(mappings) do
        vim.keymap.set("n", key, action, { buffer = bufnr, silent = true })
    end

    local last_line_nr = vim.api.nvim_buf_line_count(bufnr)
    local last_line = vim.api.nvim_buf_get_lines(bufnr, last_line_nr - 1, last_line_nr, false)[1] -- zero-indexed, exclusive end

    local last_col = string.len(last_line)

    -- Add virtual lines if provided
    if opts.virtual_lines and #opts.virtual_lines > 0 then
        local virt_ns = vim.api.nvim_create_namespace("mcphub_multiline_virtual")
        local original_line_count = #vim.split(content or "", "\n")
        local virt_lines = {}

        local divider = string.rep("-", width - 2)
        table.insert(virt_lines, { { divider, "Comment" } })
        for _, l in ipairs(opts.virtual_lines) do
            table.insert(virt_lines, vim.islist(l) and { l } or { { l, "Comment" } })
        end
        vim.api.nvim_buf_set_extmark(bufnr, virt_ns, original_line_count - 1, 0, {
            virt_lines = virt_lines,
            priority = 2000,
        })
    end

    -- Handle cursor positioning
    if opts.go_to_placeholder then
        -- Find first ${} placeholder and position cursor there
        local content_lines = vim.split(content or "", "\n")
        for i, line in ipairs(content_lines) do
            local start_pos = line:find("%${")
            if start_pos then
                vim.api.nvim_win_set_cursor(win, { i, start_pos - 1 })
                break
            end
        end
    elseif opts.start_insert ~= false then
        vim.cmd("startinsert")
        vim.api.nvim_win_set_cursor(win, { last_line_nr, last_col + 1 })
    end

    update_virtual_text() -- Show initial hint
end

function M.is_visual_mode()
    local mode = vim.fn.mode()
    return mode == "v" or mode == "V" or mode == "^V"
end

function M.get_selection(bufnr)
    bufnr = bufnr or vim.api.nvim_get_current_buf()
    local mode = vim.fn.mode()
    local start_pos, end_pos
    if M.is_visual_mode() then
        start_pos = vim.fn.getpos("v")
        end_pos = vim.fn.getpos(".")
    else
        start_pos = vim.fn.getpos("'<")
        end_pos = vim.fn.getpos("'>")
    end

    local start_line = start_pos[2]
    local start_col = start_pos[3]
    local end_line = end_pos[2]
    local end_col = end_pos[3]

    if start_line > end_line or (start_line == end_line and start_col > end_col) then
        start_line, end_line = end_line, start_line
        start_col, end_col = end_col, start_col
    end
    local lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false)
    if start_line == 0 then
        lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
        start_line = 1
        start_col = 0
        end_line = #lines
        end_col = #lines[#lines]
    end
    if #lines > 0 then
        if M.is_visual_mode() then
            start_col = 1
            end_col = #lines[#lines]
        else
            if #lines == 1 then
                lines[1] = lines[1]:sub(start_col, end_col)
            else
                lines[1] = lines[1]:sub(start_col)
                lines[#lines] = lines[#lines]:sub(1, end_col)
            end
        end
    end
    return {
        lines = lines,
        start_line = start_line,
        start_col = start_col,
        end_line = end_line,
        end_col = end_col,
    }
end

---Create a confirmation window with Yes/No/Cancel options
---@param message string | string[] | NuiLine[] Message to display
---@param opts? {relative_to_chat?: boolean, min_width?: number, max_width?: number}
---@return boolean, boolean -- (confirmed, cancelled)
function M.confirm(message, opts)
    opts = opts or {}

    local result = async.wrap(function(callback)
        if not message or #message == 0 then
            return callback(false, true)
        end

        -- Track active option (default to "yes")
        local active_option = "yes"

        -- Function to create footer with current active state
        local function create_footer()
            return {
                { " ", Text.highlights.seamless_border },
                {
                    " Yes ",
                    active_option == "yes" and Text.highlights.button_active or Text.highlights.button_inactive,
                },
                { " • ", Text.highlights.seamless_border },
                { " No ", active_option == "no" and Text.highlights.button_active or Text.highlights.button_inactive },
                { " • ", Text.highlights.seamless_border },
                {
                    " Cancel ",
                    active_option == "cancel" and Text.highlights.button_active or Text.highlights.button_inactive,
                },
                { " ", Text.highlights.seamless_border },
            }
        end

        -- Function to update footer
        local function update_footer(win)
            if vim.api.nvim_win_is_valid(win) then
                local config = vim.api.nvim_win_get_config(win)
                config.footer = create_footer()
                vim.api.nvim_win_set_config(win, config)
            end
        end

        -- Process message into lines
        local lines = {}
        if type(message) == "string" then
            lines = Text.multiline(message)
        else
            if vim.islist(message) then
                for _, line in ipairs(message) do
                    if type(line) == "string" then
                        vim.list_extend(lines, Text.multiline(line))
                    elseif vim.islist(line) then
                        local n_line = NuiLine()
                        for _, part in ipairs(line) do
                            if type(part) == "string" then
                                n_line:append(part)
                            else
                                n_line:append(unpack(part))
                            end
                        end
                        table.insert(lines, n_line)
                    else
                        local n_line = NuiLine()
                        n_line:append(line)
                        table.insert(lines, n_line)
                    end
                end
            end
        end

        -- Calculate optimal window dimensions
        local min_width = opts.min_width or 50
        local max_width = opts.max_width or 80
        local content_width = 0

        -- Find the longest line for width calculation
        for _, line in ipairs(lines) do
            local line_width = type(line) == "string" and #line or line:width()
            content_width = math.max(content_width, line_width)
        end

        -- Add padding and ensure reasonable bounds
        local width = math.max(min_width, math.min(max_width, content_width + 8))
        local height = math.min(#lines + 3, math.floor(vim.o.lines * 0.6)) -- +3 for padding and title

        -- Determine positioning - top center of editor
        local win_opts = M.get_window_position(width, height)

        -- Create buffer and set content
        local bufnr = vim.api.nvim_create_buf(false, true)
        local ns_id = vim.api.nvim_create_namespace("MCPHubConfirmPrompt")

        -- Add some padding at the top
        table.insert(lines, 1, NuiLine():append(""))

        -- Render content with proper padding
        for i, line in ipairs(lines) do
            if type(line) == "string" then
                line = Text.pad_line(line)
            elseif line._texts then
                -- This is a NuiLine object, pad it properly
                line = Text.pad_line(line)
            else
                -- This might be an array format, convert it to NuiLine first
                local nui_line = NuiLine()
                if vim.islist(line) then
                    for _, part in ipairs(line) do
                        if type(part) == "string" then
                            nui_line:append(part)
                        elseif vim.islist(part) then
                            nui_line:append(part[1], part[2])
                        end
                    end
                else
                    nui_line:append(tostring(line))
                end
                line = Text.pad_line(nui_line)
            end
            line:render(bufnr, ns_id, i)
        end

        -- Enhanced window options with better styling
        win_opts.style = "minimal"
        win_opts.border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }
        win_opts.title_pos = "center"
        win_opts.title = {
            { " MCPHUB Confirmation ", Text.highlights.header_btn },
        }
        win_opts.footer = create_footer()
        win_opts.footer_pos = "center"

        -- Create the window
        local win = vim.api.nvim_open_win(bufnr, true, win_opts)

        -- Enhanced window styling
        vim.api.nvim_win_set_option(win, "wrap", true)
        vim.api.nvim_win_set_option(win, "cursorline", false)

        -- Set buffer options
        vim.api.nvim_buf_set_option(bufnr, "modifiable", false)
        vim.api.nvim_buf_set_option(bufnr, "buftype", "nofile")
        vim.api.nvim_buf_set_option(bufnr, "swapfile", false)

        local is_closed = false

        -- Enhanced close function with cleanup
        local function close_window(confirmed, cancelled)
            if is_closed then
                return
            end
            is_closed = true

            vim.schedule(function()
                if vim.api.nvim_win_is_valid(win) then
                    if vim.api.nvim_win_is_valid(win) then
                        vim.api.nvim_win_close(win, true)
                    end
                end
                callback(confirmed, cancelled)
            end)
        end

        -- Function to execute active option
        local function execute_active_option()
            if active_option == "yes" then
                close_window(true, false)
            elseif active_option == "no" then
                close_window(false, false)
            else -- cancel
                close_window(false, true)
            end
        end

        -- Set up keymaps with visual feedback and navigation
        local keymaps = {
            -- Direct actions
            ["y"] = function()
                close_window(true, false)
            end,
            ["Y"] = function()
                close_window(true, false)
            end,
            ["n"] = function()
                close_window(false, false)
            end,
            ["N"] = function()
                close_window(false, false)
            end,
            ["c"] = function()
                close_window(false, true)
            end,
            ["C"] = function()
                close_window(false, true)
            end,
            ["<Esc>"] = function()
                close_window(false, true)
            end,
            ["q"] = function()
                close_window(false, true)
            end,
            ["<CR>"] = execute_active_option, -- Execute the currently active option
            -- Tab navigation
            ["<Tab>"] = function()
                if active_option == "yes" then
                    active_option = "no"
                elseif active_option == "no" then
                    active_option = "cancel"
                else
                    active_option = "yes"
                end
                update_footer(win)
            end,
        }

        for key, handler in pairs(keymaps) do
            vim.keymap.set("n", key, handler, {
                buffer = bufnr,
                nowait = true,
                silent = true,
                desc = "MCPHub confirm: " .. key,
            })
        end

        -- Auto-close protection
        local group = vim.api.nvim_create_augroup("MCPHubConfirm" .. bufnr, { clear = true })
        vim.api.nvim_create_autocmd({ "WinClosed", "BufWipeout" }, {
            buffer = bufnr,
            group = group,
            callback = function()
                close_window(false, true)
            end,
            once = true,
        })

        -- Focus the window and ensure it's visible
        vim.api.nvim_set_current_win(win)
        -- Ensure we're in normal mode for key navigation
        vim.cmd("stopinsert")
        vim.cmd("redraw")
    end, 1)

    return result()
end

-- Helper function to determine window positioning
---@param width number
---@param height number
---@return table window_opts
function M.get_window_position(width, height)
    local win_opts = {
        width = width,
        height = height,
        focusable = true,
        relative = "editor",
        row = 1, -- Just 1 line from the top
        col = math.floor((vim.o.columns - width) / 2), -- Center horizontally
    }

    return win_opts
end

---@param server_name string Name of the server being authorized
---@param auth_url string Authorization URL
function M.open_auth_popup(server_name, auth_url)
    local width = 80 -- Wider to accommodate wrapped text

    -- Create content using NuiLine for proper highlighting
    local info_lines = {
        Text.pad_line(
            NuiLine():append("Browser should open automatically with the authorization page.", Text.highlights.text)
        ),
        Text.pad_line(NuiLine():append("If not, you can use this URL:", Text.highlights.text)),
        Text.pad_line(NuiLine():append(auth_url, Text.highlights.link)),
        Text.empty_line(),
        Text.pad_line(
            NuiLine():append(
                "If you're running mcphub on a remote machine, paste the redirect URL below",
                Text.highlights.muted
            )
        ),
        Text.pad_line(
            NuiLine():append(
                string.format("(looks like 'http://localhost...?code=...&server_name=%s')", server_name),
                Text.highlights.muted
            )
        ),
        Text.empty_line(),
        Text.pad_line(
            NuiLine():append(
                "This popup will automatically close once authentication is successful.",
                Text.highlights.muted
            )
        ),
    }

    local info_height = #info_lines + 4
    local input_height = 1

    -- Position at top center
    local row = 1
    local col = math.floor((vim.o.columns - width) / 2)

    -- Create buffers
    local info_buf = vim.api.nvim_create_buf(false, true)
    local input_buf = vim.api.nvim_create_buf(false, true)

    -- Create namespace for virtual text
    local ns = vim.api.nvim_create_namespace("MCPHubAuthPopup")

    -- Info window configuration
    local info_opts = {
        relative = "editor",
        width = width,
        height = info_height,
        row = row,
        col = col,
        style = "minimal",
        border = "rounded",
        title = string.format(Text.icons.unauthorized .. " Authorize %s ", server_name),
        title_pos = "center",
    }

    local input_title = " > Callback URL"
    -- Input window configuration
    local input_opts = {
        relative = "editor",
        width = width,
        height = input_height,
        row = row + info_height + 2,
        col = col,
        style = "minimal",
        title = input_title,
        border = "rounded",
    }

    -- Set info content with proper highlighting
    vim.api.nvim_buf_set_option(info_buf, "modifiable", true)
    for i, line in ipairs(info_lines) do
        line:render(info_buf, ns, i)
    end
    vim.api.nvim_buf_set_option(info_buf, "modifiable", false)
    vim.api.nvim_buf_set_option(info_buf, "buftype", "nofile")

    -- Create windows
    local info_win = vim.api.nvim_open_win(info_buf, true, info_opts)
    local input_win = vim.api.nvim_open_win(input_buf, false, input_opts)

    -- Set window options
    for _, win in ipairs({ info_win, input_win }) do
        vim.api.nvim_win_set_option(win, "wrap", true)
        vim.api.nvim_win_set_option(win, "cursorline", false)
    end

    local function update_virtual_text(text, hl)
        vim.api.nvim_buf_clear_namespace(input_buf, ns, 0, -1)
        pcall(vim.api.nvim_buf_set_extmark, input_buf, ns, 0, 0, {
            virt_text = { { text, hl } },
            virt_text_pos = "right_align",
        })
    end
    update_virtual_text("[<Cr> submit, <Tab> Cycle]", Text.highlights.muted)

    -- Track current window
    local current_win = info_win

    -- Setup autocmd to close both windows
    local function close_windows()
        if vim.api.nvim_win_is_valid(info_win) then
            vim.api.nvim_win_close(info_win, true)
        end
        if vim.api.nvim_win_is_valid(input_win) then
            vim.api.nvim_win_close(input_win, true)
        end
        -- Return focus to MCPHub window
        if State.ui_instance and State.ui_instance.window then
            vim.api.nvim_set_current_win(State.ui_instance.window)
        end
    end

    local function switch_window()
        if current_win == info_win and vim.api.nvim_win_is_valid(input_win) then
            vim.api.nvim_set_current_win(input_win)
            current_win = input_win
            vim.cmd("startinsert")
        elseif current_win == input_win and vim.api.nvim_win_is_valid(info_win) then
            vim.api.nvim_set_current_win(info_win)
            current_win = info_win
            vim.cmd("stopinsert")
        end
    end
    -- Basic validation for the URL
    local function validate_url(url)
        if not url or vim.trim(url) == "" then
            return false, "Invalid URL format"
        end

        -- Check for required parameters
        if not url:match("code=[^&]+") then
            return false,
                "Missing 'code' parameter. The redirect url should look like 'http://localhost?code=...&server_name=...'."
        end
        if not url:match("server_name=[^&]+") then
            return false,
                "Missing 'server_name' parameter. The redirect url should look like 'http://localhost?code=...&server_name=...'."
        end

        return true
    end

    -- Set up auto-close on server update
    local update_handler = "MCPHubAuthPopup" .. server_name
    vim.api.nvim_create_augroup(update_handler, { clear = true })
    vim.api.nvim_create_autocmd("User", {
        pattern = "MCPHubServersUpdated",
        group = update_handler,
        callback = function()
            local server = State.hub_instance:get_server(server_name)
            if server and server.status ~= constants.ConnectionStatus.UNAUTHORIZED then
                close_windows()
                return true
            end
        end,
    })

    -- Handle input
    local function handle_submit()
        local lines = vim.api.nvim_buf_get_lines(input_buf, 0, -1, false)
        local url = vim.trim(table.concat(lines, "\n"))

        if url == "" then
            vim.notify("Please enter a callback URL", vim.log.levels.WARN)
            return
        end

        local valid, err = validate_url(url)
        if not valid then
            vim.notify(err, vim.log.levels.ERROR)
            return
        end

        local function update_input_title(text, hl)
            local ok, config = pcall(vim.api.nvim_win_get_config, input_win)
            if ok then
                config.title = { { " " .. text .. " ", hl } }
                vim.api.nvim_win_set_config(input_win, config)
            end
        end

        update_input_title(" Authorizing...", Text.highlights.info)
        update_virtual_text("Validating callback URL...", Text.highlights.info)
        State.hub_instance:handle_oauth_callback(url, function(response, err)
            if err then
                vim.notify("OAuth callback failed: " .. err, vim.log.levels.ERROR)
            else
                vim.notify(response.message, vim.log.levels.INFO)
            end
            update_input_title(input_title, Text.highlights.info)
            update_virtual_text("Press <Cr> to submit", Text.highlights.comment)
        end)
    end

    -- Set up keymaps
    local function setup_keymaps(buf)
        local keymaps = {
            ["<CR>"] = handle_submit,
            ["<Esc>"] = close_windows,
            ["q"] = close_windows,
            ["<Tab>"] = switch_window,
        }

        for key, handler in pairs(keymaps) do
            vim.keymap.set("n", key, handler, { buffer = buf, nowait = true })
            vim.keymap.set("i", key, handler, { buffer = buf, nowait = true })
        end
    end

    setup_keymaps(info_buf)
    setup_keymaps(input_buf)

    -- Set up buffer cleanup
    for _, buf in ipairs({ info_buf, input_buf }) do
        vim.api.nvim_create_autocmd("BufWipeout", {
            buffer = buf,
            callback = close_windows,
            once = true,
        })
    end
    switch_window()
end

return M

---@brief [[
--- Config view for MCPHub UI
--- Shows MCP server configurations
---@brief ]]
local NuiLine = require("mcphub.utils.nuiline")
local Text = require("mcphub.utils.text")
local View = require("mcphub.ui.views.base")

---@class ConfigView:View
---@field current_tab_index number Currently selected tab index
---@field config_files string[] List of config file paths
local ConfigView = setmetatable({}, {
    __index = View,
})
ConfigView.__index = ConfigView

function ConfigView:new(ui)
    local instance = View:new(ui, "config") -- Create base view with name
    instance.current_tab_index = 1
    instance.config_files = {}
    return setmetatable(instance, ConfigView)
end

function ConfigView:before_enter()
    View.before_enter(self)

    -- Get active config files from ConfigManager
    local ConfigManager = require("mcphub.utils.config_manager")
    self.config_files = ConfigManager.get_active_config_files()

    -- Reverse the order of config files for tab display
    local reversed_files = {}
    for i = #self.config_files, 1, -1 do
        table.insert(reversed_files, self.config_files[i])
    end
    self.config_files = reversed_files

    -- Ensure we have a valid tab index
    if #self.config_files == 0 then
        self.current_tab_index = 1
    else
        self.current_tab_index = math.min(self.current_tab_index, #self.config_files)
    end

    -- Build keymaps based on available files
    self.keymaps = {
        ["e"] = {
            action = function()
                if #self.config_files > 0 then
                    local current_file = self.config_files[self.current_tab_index]
                    self.ui:toggle()
                    vim.cmd("edit " .. current_file)
                else
                    vim.notify("No configuration file available", vim.log.levels.ERROR)
                end
            end,
            desc = "Edit config",
        },
    }

    -- Add tab navigation keymaps only if multiple files exist
    if #self.config_files > 1 then
        self.keymaps["<Tab>"] = {
            action = function()
                self.current_tab_index = (self.current_tab_index % #self.config_files) + 1
                self:draw()
            end,
            desc = "Next tab",
        }
        self.keymaps["<S-Tab>"] = {
            action = function()
                self.current_tab_index = ((self.current_tab_index - 2) % #self.config_files) + 1
                self:draw()
            end,
            desc = "Previous tab",
        }
    end
end

function ConfigView:get_initial_cursor_position()
    -- Position at start of server configurations
    local lines = self:render_header()

    -- Add tab bar height if we have multiple files
    if #self.config_files > 0 then
        return #lines + 4 -- header + tab bar + separator
    else
        return #lines + 3 -- header + file info
    end
end

function ConfigView:render()
    -- Get base header
    local lines = self:render_header(false)

    if #self.config_files == 0 then
        -- No config files available
        table.insert(lines, Text.pad_line(NuiLine():append("No configuration files available", Text.highlights.warn)))
        return lines
    end

    -- Render tab bar if multiple files
    if #self.config_files > 0 then
        local tabs = {}
        for i, file_path in ipairs(self.config_files) do
            local file_name = vim.fn.fnamemodify(file_path, ":t") -- Get just filename
            local icon = Text.icons.folder -- Default to workspace/project icon

            -- Last file is treated as global
            if i == #self.config_files then
                icon = Text.icons.globe
            end

            table.insert(tabs, {
                text = icon .. " " .. file_name,
                selected = i == self.current_tab_index,
            })
        end

        table.insert(lines, Text.create_tab_bar(tabs, self:get_width()))
        table.insert(lines, Text.empty_line())
    end

    -- Show current file path
    local current_file = self.config_files[self.current_tab_index]
    if current_file then
        local file_line = NuiLine():append(Text.icons.file .. " " .. current_file, Text.highlights.muted)
        table.insert(lines, Text.pad_line(file_line))
    end

    table.insert(lines, Text.empty_line())

    -- Get and validate current file content
    local ConfigManager = require("mcphub.utils.config_manager")
    local file_content, file_read_error = ConfigManager.get_file_content_json(current_file)

    if not file_content then
        -- table.insert(lines, self:center(NuiLine():append("Failed to load configuration file", Text.highlights.error)))
        vim.list_extend(lines, vim.tbl_map(Text.pad_line, Text.multiline(file_read_error or "", Text.highlights.error)))
        table.insert(lines, Text.empty_line())
    else
        -- Show file content with JSON highlighting
        vim.list_extend(lines, Text.render_json(file_content, { use_jq = true }))
    end

    return lines
end

return ConfigView

local highlights = require("mcphub.utils.highlights").groups
local ImageCache = require("mcphub.utils.image_cache")
local NuiLine = require("mcphub.utils.nuiline")
local Text = require("mcphub.utils.text")
local ui_utils = require("mcphub.utils.ui")

---@class CapabilityHandler
local CapabilityHandler = {
    type = nil, -- to be set by subclasses
}
CapabilityHandler.__index = CapabilityHandler

function CapabilityHandler:new(server_name, capability_info, view)
    ---@class CapabilityHandler
    local handler = setmetatable({
        name = capability_info.def
                and (capability_info.def.name or capability_info.def.uri or capability_info.def.uriTemplate)
            or (capability_info.name or ""),
        server_name = server_name,
        info = capability_info,
        def = capability_info.def or {},
        view = view,
        state = {
            is_executing = false,
            result = nil,
            error = nil,
        },
        interactive_lines = {},
    }, self)
    return handler
end

--- Get preferred cursor position when entering capability mode
---@return number[]|nil Line number to position cursor at
function CapabilityHandler:get_cursor_position()
    -- Default to first interactive line if any
    if #self.interactive_lines > 0 then
        return { self.interactive_lines[1].line, 2 }
    end
    return nil
end

-- Line tracking for interactivity
function CapabilityHandler:track_line(line_nr, type, context)
    table.insert(self.interactive_lines, {
        line = line_nr,
        type = type,
        context = context,
    })
end

function CapabilityHandler:clear_line_tracking()
    self.interactive_lines = {}
end

function CapabilityHandler:get_line_info(line_nr)
    for _, tracked in ipairs(self.interactive_lines) do
        if tracked.line == line_nr then
            return tracked.type, tracked.context
        end
    end
    return nil, nil
end

-- Common highlighting
function CapabilityHandler:handle_cursor_move(view, line)
    local type, _ = self:get_line_info(line)
    if not type then
        return
    end

    if type == "submit" and not self.state.is_executing then
        view.cursor_highlight = vim.api.nvim_buf_set_extmark(view.ui.buffer, view.hover_ns, line - 1, 0, {
            virt_text = { { "[<l> Submit]", highlights.muted } },
            virt_text_pos = "eol",
        })
    elseif type == "input" then
        view.cursor_highlight = vim.api.nvim_buf_set_extmark(view.ui.buffer, view.hover_ns, line - 1, 0, {
            virt_text = { { "[<l> Edit, <o> Multiline Edit]", highlights.muted } },
            virt_text_pos = "eol",
        })
    end
end

-- Input handling
function CapabilityHandler:handle_input(prompt, default, callback)
    vim.ui.input({
        prompt = prompt,
        default = default or "",
    }, function(input)
        if input ~= nil then -- Only handle if not cancelled
            callback(input)
        end
    end)
end

-- Text box handling
function CapabilityHandler:open_text_box(...)
    ui_utils.multiline_input(...)
end

-- Common section rendering utilities
function CapabilityHandler:render_section_start(title, highlight)
    local lines = {}
    table.insert(
        lines,
        Text.pad_line(
            NuiLine():append("╭─", highlights.muted):append(" " .. title .. " ", highlight or highlights.header)
        )
    )
    return lines
end

function CapabilityHandler:render_section_content(content, indent_level)
    local lines = {}
    local padding = string.rep(" ", indent_level or 1)
    for _, line in ipairs(content) do
        local rendered_line = NuiLine()
        rendered_line:append("│", highlights.muted):append(padding, highlights.muted):append(line)
        table.insert(lines, Text.pad_line(rendered_line))
    end
    return lines
end

function CapabilityHandler:render_section_end(content, indent_level)
    local lines = {}
    local padding = string.rep(" ", indent_level or 1)
    if content then
        for _, line in ipairs(content) do
            local rendered_line = NuiLine()
            rendered_line:append("╰─", highlights.muted):append(padding, highlights.muted):append(line)
            table.insert(lines, Text.pad_line(rendered_line))
        end
    else
        table.insert(lines, Text.pad_line(NuiLine():append("╰─", highlights.muted)))
    end
    return lines
end

function CapabilityHandler:render_output(output)
    local lines = {}
    --Handle text content
    if output.text and output.text ~= "" then
        vim.list_extend(lines, self:render_section_content(Text.multiline(output.text, highlights.info), 1))
    end
    -- Handle image content
    if output.images and #output.images > 0 then
        if #lines > 0 then
            vim.list_extend(lines, self:render_section_content({ "  " }, 1))
        end
        for i, img in ipairs(output.images) do
            -- Save to temp file
            local ok, filepath = pcall(ImageCache.save_image, img.data, img.mimeType or "application/octet-stream")
            if ok and filepath then
                -- Create filesystem URL
                local url = "file://" .. filepath
                -- Show friendly name with URL
                local image_line = NuiLine()
                    :append("Image " .. i .. ": ", highlights.muted)
                    :append(" [", highlights.muted)
                    :append(url, highlights.link)
                    :append("]", highlights.muted)
                vim.list_extend(lines, self:render_section_content({ image_line }, 1))
            else
                vim.list_extend(lines, self:render_section_content({ "Failed to save image: " .. filepath }, 1))
            end
        end
    end

    if output.audios and #output.audios > 0 then
        if #lines > 0 then
            vim.list_extend(lines, self:render_section_content({ "  " }, 1))
        end
        for i, _ in ipairs(output.audios) do
            local audio_line = NuiLine():append("Audio " .. i .. ": Audio data cannot be shown", highlights.muted)
            vim.list_extend(lines, self:render_section_content({ audio_line }, 1))
        end
    end

    --Handle blobs content
    if output.blobs and #output.blobs > 0 then
        if #lines > 0 then
            vim.list_extend(lines, self:render_section_content({ "  " }, 1))
        end
        for i, _ in ipairs(output.blobs) do
            local blob_line = NuiLine():append("Blob " .. i .. ": Blob data cannot be shown", highlights.muted)
            vim.list_extend(lines, self:render_section_content({ blob_line }, 1))
        end
    end
    return lines
end

-- Common result rendering
function CapabilityHandler:render_result()
    if not self.state.result then
        return {}
    end

    local lines = {}
    table.insert(lines, Text.pad_line(NuiLine())) -- Empty line
    vim.list_extend(lines, self:render_section_start("Result"))
    vim.list_extend(lines, self:render_output(self.state.result))
    vim.list_extend(lines, self:render_section_end())
    return lines
end

-- Error handling
function CapabilityHandler:handle_response(response, err)
    self.state.is_executing = false
    if err then
        vim.notify(string.format("%s execution failed: %s", self.type, err), vim.log.levels.ERROR)
        self.state.error = err
        self.state.result = response
    else
        if response.error then
            self.state.error = response.error
            self.state.result = response
        else
            self.state.result = response
            self.state.error = nil
        end
    end
end

function CapabilityHandler:get_description(def_description)
    local description = def_description or self.def.description or ""
    if type(description) == "function" then
        local ok, desc = pcall(description, self.def)
        if not ok then
            description = "Failed to get description :" .. (desc or "")
        else
            description = "(" .. Text.icons.event .. " Dynamic) " .. (desc or "Nothing returned")
        end
    end
    return description
end

-- Abstract methods to be implemented by subclasses
function CapabilityHandler:execute()
    error(string.format("execute() not implemented for capability type: %s", self.type))
end

function CapabilityHandler:handle_action(...)
    error(string.format("handle_action() not implemented for capability type: %s", self.type))
end

---@return NuiLine[]
function CapabilityHandler:render(...)
    error(string.format("render() not implemented for capability type: %s", self.type))
end

return CapabilityHandler

local Base = require("mcphub.ui.capabilities.base")
local NuiLine = require("mcphub.utils.nuiline")
local Text = require("mcphub.utils.text")
local highlights = require("mcphub.utils.highlights").groups
local Installers = require("mcphub.utils.installers")
local prompt_utils = require("mcphub.utils.prompt")

---@class CreateServerHandler : CapabilityHandler
local CreateServerHandler = setmetatable({}, { __index = Base })
CreateServerHandler.__index = CreateServerHandler
CreateServerHandler.type = "create_server"

function CreateServerHandler:new(server_name, capability_info, view)
    local handler = Base.new(self, server_name, capability_info, view)
    return handler
end

function CreateServerHandler:handle_action(line)
    local type = self:get_line_info(line)
    if type == "install" then
        -- Get available installers
        local available = {}
        for id, installer in pairs(Installers) do
            if installer.check() then
                table.insert(available, {
                    id = id,
                    name = installer.name,
                })
            end
        end

        if #available > 0 then
            vim.ui.select(available, {
                prompt = "Choose installer:",
                format_item = function(item)
                    return item.name
                end,
            }, function(choice)
                if choice then
                    local installer = Installers[choice.id]
                    if installer then
                        self.view.ui:cleanup()
                        installer:create_native_server()
                    end
                end
            end)
        else
            vim.notify("No installers available. Please install CodeCompanion or Avante.", vim.log.levels.ERROR)
        end
    end
end

function CreateServerHandler:render(line_offset)
    line_offset = line_offset or 0
    self:clear_line_tracking()

    local lines = {}

    -- Description text
    local description = {
        "Native Lua servers allow you to create custom MCP-compatible servers directly in Lua.",
        "These servers can provide tools and resources that integrate seamlessly with mcphub.nvim.",
        "Perfect for plugin-specific functionality, file operations, or any custom Neovim integration.",
        "Create your own server to extend mcphub with your unique tools and resources.",
    }

    for _, line in ipairs(description) do
        table.insert(lines, Text.pad_line(line, highlights.muted))
    end

    table.insert(lines, Text.empty_line())

    local install_line = NuiLine()
        :append(" " .. Text.icons.pencil .. " ", highlights.header_btn)
        :append("Create ", highlights.header_btn)
        :append(" with: ", highlights.muted)

    -- Check each installer
    for id, installer in pairs(Installers) do
        if installer.check() then
            install_line
                :append("[" .. installer.name .. "]", highlights.success)
                :append(" ", highlights.muted)
                :append(Text.icons.check, highlights.success)
                :append("  ", highlights.muted)
        else
            install_line
                :append("[" .. installer.name .. "]", highlights.error)
                :append(" ", highlights.muted)
                :append(Text.icons.uninstall, highlights.error)
                :append("  ", highlights.muted)
        end
    end

    table.insert(lines, Text.pad_line(install_line))
    -- Track install line for interaction
    self:track_line(#lines + line_offset, "install", {
        hint = "[<l> Select Installer]",
    })

    table.insert(lines, Text.empty_line())
    table.insert(lines, self.view:divider())
    table.insert(lines, Text.empty_line())

    -- Show the LLM prompt content
    local guide = prompt_utils.get_native_server_prompt()
    if guide then
        vim.list_extend(lines, Text.render_markdown(guide))
    else
        table.insert(lines, Text.pad_line("Native server guide not found", highlights.error))
    end
    return lines
end

return CreateServerHandler

---@brief [[
--- Text utilities for MCPHub
--- Provides text formatting, layout, and rendering utilities
---@brief ]]
local NuiLine = require("mcphub.utils.nuiline")
local hl = require("mcphub.utils.highlights")

local M = {}

-- Constants
M.HORIZONTAL_PADDING = 2

-- Export highlight groups for easy access
M.highlights = hl.groups

M.icons = {

    hourglass = "",
    person = "",
    clock = "",
    server = "󰒋",
    antenna = "󰖩",
    antenna_off = "󰖪",
    sse = "",
    auto = "󰁪",
    tower = "󰐻",
    tool = "",
    resourceTemplate = "",
    resource = "",
    circle = "○",
    circleFilled = "●",
    bug = "",
    event = "",
    param = "",
    favorite = "",
    loaded = "●",
    vertical_bar = "│",
    not_loaded = "○",
    arrowRight = "➜",
    triangleDown = "▼",
    triangleRight = "▶",
    search = "",
    tag = "",
    sort = "",
    octoface = "",
    check = "✔",
    gear = "",
    loading = "",
    downArrow = "",
    uninstall = "",
    sparkles = "✨",
    download = "",
    install = "",
    link = "",
    pencil = "󰏫",
    edit = "󰏫",
    plus = "",
    instructions = "",
    unauthorized = "󱅞",

    file = "",
    folder = "",
    globe = "",
    prompt = "󰿠",
    -- Error type icons
    setup_error = "",
    server_error = "",
    runtime_error = "",
    general_error = "",

    error = "",
    warn = "",
    info = "",
    question = "",
    hint = "",
    debug = "",
    trace = "✎",
}

--- Split text into multiple NuiLines while preserving newlines
---@param content string Text that might contain newlines
---@param highlight? string Optional highlight group
---@return NuiLine[]
function M.multiline(content, highlight)
    if type(content) ~= "string" then
        content = tostring(content)
    end
    local lines = {}
    for _, line in
        ipairs(vim.split(content, "\n", {
            plain = true,
        }))
    do
        table.insert(lines, NuiLine():append(line, highlight))
    end
    return lines
end

--- Add horizontal padding to a line
---@param line NuiLine|string The line to pad
---@param highlight? string Optional highlight for string input
---@param padding? number Override default padding
---@return NuiLine
function M.pad_line(line, highlight, padding)
    local nui_line = NuiLine():append(string.rep(" ", padding or M.HORIZONTAL_PADDING))

    if type(line) == "string" then
        nui_line:append(line, highlight)
    else
        nui_line:append(line)
    end

    return nui_line:append(string.rep(" ", padding or M.HORIZONTAL_PADDING))
end

--- Create empty line with consistent padding
---@return NuiLine
function M.empty_line()
    return M.pad_line("")
end

--- Create a divider line
---@param width number Total width
---@param is_full? boolean Whether to ignore padding
---@return NuiLine
function M.divider(width, is_full)
    if is_full then
        return NuiLine():append(string.rep("-", width), M.highlights.muted)
    end
    return M.pad_line(string.rep("-", width - (M.HORIZONTAL_PADDING * 2)), M.highlights.muted)
end

--- Align text with proper padding
---@param text string|NuiLine Text to align
---@param width number Available width
---@param align "left"|"center"|"right" Alignment direction
---@param highlight? string Optional highlight for text
---@return NuiLine
function M.align_text(text, width, align, highlight)
    local inner_width = width - (M.HORIZONTAL_PADDING * 2)

    -- Convert string to NuiLine if needed
    local line = type(text) == "string" and NuiLine():append(text, highlight) or text --[[@as NuiLine]]
    local line_width = line:width()

    -- Calculate padding
    local padding = math.max(0, inner_width - line_width)
    local left_pad = align == "center" and math.floor(padding / 2) or align == "right" and padding or 0
    -- local right_pad = align == "center" and math.ceil(padding / 2) or align == "left" and padding or 0

    -- Create padded line
    return NuiLine():append(string.rep(" ", M.HORIZONTAL_PADDING + left_pad)):append(line)
    -- :append(string.rep(" ", right_pad + M.HORIZONTAL_PADDING))
end

---@param label string
---@param shortcut string
---@param selected boolean
---@return NuiLine
function M.create_button(label, shortcut, selected)
    local line = NuiLine()
    -- Start button group
    if selected then
        -- Selected button has full background
        line:append(" " .. shortcut, M.highlights.header_btn_shortcut)
        line:append(" " .. label .. " ", M.highlights.header_btn)
    else
        -- Unselected shows just shortcut highlighted
        line:append(" " .. shortcut, M.highlights.header_shortcut)
        line:append(" " .. label .. " ", M.highlights.header)
    end
    return line
end

--- Create centered tab bar with selected state
---@param tabs {text: string, selected: boolean}[] Array of tab objects
---@param width number Total width available
---@return NuiLine
function M.create_tab_bar(tabs, width)
    -- Create tab group first
    local tab_group = NuiLine()
    for i, tab in ipairs(tabs) do
        if i > 1 then
            tab_group:append(" ")
        end
        tab_group:append(
            " " .. tab.text .. " ",
            tab.selected and M.highlights.button_active or M.highlights.button_inactive
        )
    end

    -- Create the entire line with centered tab group
    return M.align_text(tab_group, width, "center")
end

--- The MCP Hub logo
---@param width number Window width for centering
---@return NuiLine[]
function M.render_logo(width)
    local logo_lines = {
        "╔╦╗╔═╗╔═╗  ╦ ╦╦ ╦╔╗ ",
        "║║║║  ╠═╝  ╠═╣║ ║╠╩╗",
        "╩ ╩╚═╝╩    ╩ ╩╚═╝╚═╝",
    }
    local lines = {}
    for _, line in ipairs(logo_lines) do
        table.insert(lines, M.align_text(line, width, "center", M.highlights.title))
    end
    return lines
end

--- Create header with buttons
---@param width number Window width
---@param current_view string Currently selected view
---@return NuiLine[]
function M.render_header(width, current_view)
    local lines = M.render_logo(width)

    -- Create buttons line
    local buttons = NuiLine()

    -- Add buttons with proper padding
    local btn_list = {
        {
            key = "H",
            label = "Hub",
            view = "main",
        },
        {
            key = "M",
            label = "Marketplace",
            view = "marketplace",
        },
        {
            key = "C",
            label = "Config",
            view = "config",
        },
        {
            key = "L",
            label = "Logs",
            view = "logs",
        },
        {
            key = "?",
            label = "Help",
            view = "help",
        },
    }

    for i, btn in ipairs(btn_list) do
        if i > 1 then
            buttons:append("  ") -- Add spacing between buttons
        end
        buttons:append(M.create_button(btn.label, btn.key, current_view == btn.view))
    end

    -- Center the buttons line
    local padding = math.floor((width - buttons:width()) / 2)
    if padding > 0 then
        table.insert(lines, NuiLine():append(string.rep(" ", padding)):append(buttons))
    else
        table.insert(lines, buttons)
    end

    return lines
end

--- Render markdown text with proper syntax highlighting
---@param text string The markdown text to render
---@return NuiLine[]
function M.render_markdown(text)
    if not text then
        return {}
    end

    local lines = {}
    local in_code_block = false

    for _, line in ipairs(vim.split(text, "\n", { plain = true })) do
        local nui_line = NuiLine()

        -- Handle code blocks
        if line:match("^```") then
            in_code_block = not in_code_block
            nui_line:append(line, M.highlights.muted)

        -- Inside code block
        elseif in_code_block then
            nui_line:append(line, M.highlights.code)

        -- Headers
        elseif line:match("^#+ ") then
            local level = #line:match("^(#+)")
            local header_text = line:match("^#+%s+(.+)")
            nui_line:append(string.rep("#", level) .. " ", M.highlights.muted):append(header_text, M.highlights.title)

        -- Lists
        elseif line:match("^%s*[-*] ") then
            local indent = #(line:match("^%s*") or "")
            local list_text = line:match("^%s*[-*]%s+(.+)")
            nui_line
                :append(string.rep(" ", indent))
                :append("• ", M.highlights.muted)
                :append(list_text, M.highlights.text)

        -- Normal text
        else
            nui_line:append(line, M.highlights.text)
        end

        table.insert(lines, M.pad_line(nui_line))
    end

    return lines
end

--- Render JSON with syntax highlighting using the existing pretty_json formatter
---@param text string|table? The JSON text or table to render
---@param opts table Optional options
---@return NuiLine[]
function M.render_json(text, opts)
    local utils = require("mcphub.utils")

    -- Convert table to JSON if needed
    if type(text) == "table" then
        text = vim.json.encode(text)
    end

    -- Use the existing pretty printer
    local formatted = utils.pretty_json(text or "", opts)
    local lines = {}

    -- Process each line and add highlighting
    for _, line in ipairs(vim.split(formatted, "\n", { plain = true })) do
        local nui_line = NuiLine()
        local pos = 1

        -- Add indentation
        local indent = line:match("^(%s*)")
        if indent then
            nui_line:append(indent)
            pos = #indent + 1
        end

        -- Handle property names (with quotes) first
        local property = line:match('^%s*"([^"]+)"%s*:', pos)
        if property then
            nui_line:append('"' .. property .. '"', M.highlights.json_property)
            pos = pos + #property + 2 -- +2 for quotes
            -- Skip past the colon
            local colon_pos = line:find(":", pos)
            if colon_pos then
                nui_line:append(line:sub(pos, colon_pos), M.highlights.json_punctuation)
                pos = colon_pos + 1
            end
        end

        -- Process the rest of the line
        while pos <= #line do
            local char = line:sub(pos, pos)

            -- Handle structural characters
            if char:match("[{%[%]}:,]") then
                nui_line:append(char, M.highlights.json_punctuation)
            -- Handle string values (must be in quotes)
            elseif char == '"' then
                local str_end = pos + 1
                while str_end <= #line do
                    if line:sub(str_end, str_end) == '"' and line:sub(str_end - 1, str_end - 1) ~= "\\" then
                        break
                    end
                    str_end = str_end + 1
                end
                nui_line:append(line:sub(pos, str_end), M.highlights.json_string)
                pos = str_end
            -- Handle numbers
            elseif char:match("%d") then
                local num = line:match("%d+%.?%d*", pos)
                if num then
                    nui_line:append(num, M.highlights.json_number)
                    pos = pos + #num - 1
                end
            -- Handle boolean and null
            elseif line:match("^true", pos) then
                nui_line:append("true", M.highlights.json_boolean)
                pos = pos + 3
            elseif line:match("^false", pos) then
                nui_line:append("false", M.highlights.json_boolean)
                pos = pos + 4
            elseif line:match("^null", pos) then
                nui_line:append("null", M.highlights.json_null)
                pos = pos + 3
            -- Handle whitespace
            elseif char:match("%s") then
                nui_line:append(char)
            end
            pos = pos + 1
        end

        table.insert(lines, M.pad_line(nui_line))
    end

    return lines
end

return M

local Base = require("mcphub.ui.capabilities.base")
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local highlights = require("mcphub.utils.highlights").groups
local Handlers = require("mcphub.utils.handlers")
local log = require("mcphub.utils.log")

---@class PromptHandler : CapabilityHandler
local PromptHandler = setmetatable({}, {
    __index = Base,
})
PromptHandler.__index = PromptHandler
PromptHandler.type = "prompt"
PromptHandler.arguments = {}

function PromptHandler:new(server_name, capability_info, view)
    local instance = Base:new(server_name, capability_info, view)
    setmetatable(instance, PromptHandler)
    instance.state = vim.tbl_extend("force", instance.state, {
        params = {
            values = {},
            errors = {},
        },
    })
    return instance
end

function PromptHandler:format_param_type(param)
    local handler = Handlers.TypeHandlers[param.type]
    if not handler then
        return param.type
    end
    return handler.format(param)
end

function PromptHandler:validate_all_params()
    if not self.arguments then
        return true, nil, {}
    end

    local errors = {}
    for _, argument in ipairs(self.arguments) do
        local value = self.state.params.values[argument.name]

        -- Check required fields
        if argument.required and (not value or value == "") then
            errors[argument.name] = "Required argument"
        -- Only validate non-empty values
        elseif value and value ~= "" then
            if type(value) ~= "string" then
                errors[argument.name] = "Invalid type, expected string"
            end
        end
        -- Skip validation for empty optional fields
    end

    if next(errors) then
        return false, "Some required arguments are missing or invalid", errors
    end

    return true, nil, {}
end

-- Action handling
-- Common callback logic for input handling
function PromptHandler:handle_param_update(param_name, input)
    -- Clear previous error
    self.state.params.errors[param_name] = nil
    local param = {}
    for _, v in ipairs(self.arguments) do
        if v.name == param_name then
            param = v
        end
    end

    -- Handle empty input
    if input == "" then
        -- Check if field is required
        local is_required = param.required == true
        if is_required then
            self.state.params.errors[param_name] = "Required parameter"
        else
            -- For optional fields, clear value and error
            self.state.params.values[param_name] = nil
        end
    else
        self.state.params.values[param_name] = input
    end
    self.view:draw()
end

function PromptHandler:handle_input_action(param_name)
    self:handle_input(
        string.format("%s (%s): ", param_name, "string"),
        self.state.params.values[param_name],
        function(input)
            self:handle_param_update(param_name, input)
        end
    )
end

function PromptHandler:handle_text_box(line)
    local type, context = self:get_line_info(line)
    if type == "input" then
        local param_name = context
        self:open_text_box(
            string.format("%s (%s)", param_name, "string"),
            self.state.params.values[param_name] or "",
            function(input)
                self:handle_param_update(param_name, input)
            end
        )
    end
end

function PromptHandler:handle_action(line)
    local type, context = self:get_line_info(line)
    if not type then
        return
    end

    if type == "input" then
        self:handle_input_action(context)
    elseif type == "submit" then
        self:execute()
    end
end

-- Execution
function PromptHandler:execute()
    -- Check if already executing
    if self.state.is_executing then
        vim.notify("Getting prompt...", vim.log.levels.WARN)
        return
    end

    -- Validate all parameters first
    local ok, err, errors = self:validate_all_params()
    self.state.params.errors = errors
    self.state.error = err
    if not ok then
        self.view:draw()
        return
    end

    -- Set executing state
    self.state.is_executing = true
    self.state.error = nil
    self.view:draw()

    -- Convert all parameters to their proper types
    local converted_values = {}
    for name, value in pairs(self.state.params.values) do
        converted_values[name] = value
    end

    log.debug(string.format("Getting prompt %s with parameters: %s", self.def.name, vim.inspect(converted_values)))
    -- Execute tool
    if State.hub_instance then
        State.hub_instance:get_prompt(self.server_name, self.def.name, converted_values, {
            caller = {
                type = "hubui",
                hubui = State.ui_instance,
            },
            parse_response = true,
            callback = function(response, error)
                self:handle_response(response, error)
                self.view:draw()
            end,
        })
    end
end

function PromptHandler:render_submit()
    local submit_content
    if self.state.is_executing then
        submit_content = NuiLine():append("[ " .. Text.icons.event .. " Processing... ]", highlights.muted)
    else
        submit_content = NuiLine():append("[ " .. Text.icons.gear .. " Submit ]", highlights.success_fill)
    end
    return submit_content
end

-- Rendering
function PromptHandler:render_param_form(line_offset)
    -- Clear previous line tracking
    self:clear_line_tracking()

    local lines = {}

    local arguments = self.def.arguments
    local is_function = type(arguments) == "function"
    -- Parameters section
    vim.list_extend(
        lines,
        self:render_section_start((is_function and "(" .. Text.icons.event .. " Dynamic) " or "") .. "Input Params")
    )
    arguments = self:get_arguments(arguments)
    self.arguments = arguments
    if not arguments or not next(arguments) then
        -- No parameters case
        local placeholder = NuiLine():append("No arguments required ", highlights.muted)

        -- Submit button
        local submit_content = self:render_submit()
        vim.list_extend(
            lines,
            self:render_section_content({ placeholder, NuiLine():append(" ", highlights.muted), submit_content }, 2)
        )

        -- Track submit line
        self:track_line(line_offset + #lines, "submit")
    else
        -- Render each parameter
        for _, argument in ipairs(arguments) do
            -- Parameter name and type
            local name_line = NuiLine()
                :append(argument.required and "* " or "  ", highlights.error)
                :append(argument.name, highlights.success)
                :append(string.format(" (%s)", "string"), highlights.muted)
            vim.list_extend(lines, self:render_section_content({ name_line }, 2))

            -- Description if any
            if argument.description then
                for _, desc_line in ipairs(Text.multiline(argument.description, highlights.muted)) do
                    vim.list_extend(lines, self:render_section_content({ desc_line }, 4))
                end
            end

            -- Input field
            local value = self.state.params.values[argument.name]
            local input_line = NuiLine():append("> ", highlights.success):append(value or "", highlights.info)
            vim.list_extend(lines, self:render_section_content({ input_line }, 2))

            -- Track input line
            self:track_line(line_offset + #lines, "input", argument.name)

            -- Error if any
            if self.state.params.errors[argument.name] then
                local error_lines = Text.multiline(self.state.params.errors[argument.name], highlights.error)
                vim.list_extend(lines, self:render_section_content(error_lines, 2))
            end

            table.insert(lines, Text.pad_line(NuiLine():append("│", highlights.muted)))
        end

        -- Submit button
        local submit_content = self:render_submit()
        vim.list_extend(lines, self:render_section_content({ submit_content }, 2))

        -- Track submit line
        self:track_line(line_offset + #lines, "submit")
    end

    -- Error message
    if self.state.error then
        local error_lines = Text.multiline(self.state.error, highlights.error)
        vim.list_extend(lines, self:render_section_content(error_lines, 2))
    end
    vim.list_extend(lines, self:render_section_end())
    return lines
end

function PromptHandler:get_arguments(arguments)
    local base = {}
    arguments = self.def.arguments
    if not arguments or (type(arguments) ~= "function" and not next(arguments or {})) then
        arguments = base
    end
    local parsedArguments = arguments
    if type(arguments) == "function" then
        local ok, schema = pcall(arguments, self.def)
        if not ok then
            local err = "Error in arguments function: " .. tostring(schema)
            self.state.error = err
            log.error(err)
            parsedArguments = base
        else
            parsedArguments = schema or base
        end
    end
    local function validate_arguments(args, name)
        for _, arg in ipairs(args) do
            if type(arg) ~= "table" then
                return {
                    ok = false,
                    error = "Argument should be a table in " .. name .. " prompt",
                }
            end
            if not arg.name or arg.name == "" then
                return {
                    ok = false,
                    error = "Argument name is required in " .. name .. " prompt",
                }
            end
        end
        return { ok = true }
    end
    local res = validate_arguments(parsedArguments, self.def.name)
    if not res.ok then
        local err = "Error in arguments function: " .. tostring(res.error)
        self.state.error = err
        log.error(err)
        return base
    end
    return parsedArguments
end

function PromptHandler:render(line_offset)
    line_offset = line_offset or 0
    local lines = {}
    vim.list_extend(lines, vim.tbl_map(Text.pad_line, Text.multiline(self:get_description(), highlights.muted)))
    table.insert(lines, Text.pad_line(NuiLine()))
    -- Parameter form
    vim.list_extend(lines, self:render_param_form(line_offset + #lines))
    table.insert(lines, Text.pad_line(NuiLine())) -- Empty line

    if not self.state.result then
        return lines
    end

    vim.list_extend(lines, self:render_section_start("Result"))

    -- Handle text content
    if self.state.result then
        ---@diagnostic disable-next-line: undefined-field
        local messages = self.state.result.messages or {}
        for _, message in ipairs(messages) do
            vim.list_extend(lines, self:render_section_content({ " " }))
            local role = message.role
            local output = message.output
            vim.list_extend(lines, self:render_section_content({ NuiLine():append(role, highlights.success) }))
            vim.list_extend(lines, self:render_output(output))
        end
    end

    vim.list_extend(lines, self:render_section_end())
    return lines
end

return PromptHandler

local Base = require("mcphub.ui.capabilities.base")
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local highlights = require("mcphub.utils.highlights").groups
local Handlers = require("mcphub.utils.handlers")
local log = require("mcphub.utils.log")
local validation = require("mcphub.utils.validation")

---@class ToolHandler : CapabilityHandler
local ToolHandler = setmetatable({}, {
    __index = Base,
})
ToolHandler.__index = ToolHandler
ToolHandler.type = "tool"
ToolHandler.parsedInputSchema = {}

function ToolHandler:new(server_name, capability_info, view)
    local instance = Base:new(server_name, capability_info, view)
    setmetatable(instance, ToolHandler)
    instance.state = vim.tbl_extend("force", instance.state, {
        params = {
            values = {},
            errors = {},
        },
    })
    return instance
end

-- Parameter ordering
function ToolHandler:get_ordered_params(inputSchema)
    if not inputSchema or not inputSchema.properties then
        return {}
    end

    local params = {}
    for name, prop in pairs(inputSchema.properties) do
        table.insert(params, {
            name = name,
            type = prop.type,
            description = prop.description,
            required = vim.tbl_contains(inputSchema.required or {}, name),
            default = prop.default,
            value = self.state.params.values[name],
        })
    end

    -- Sort by required first, then name
    table.sort(params, function(a, b)
        if a.required ~= b.required then
            return a.required
        end
        return a.name < b.name
    end)

    return params
end

-- Parameter handling
function ToolHandler:validate_param(name, value)
    local param_schema = self.parsedInputSchema.properties[name]
    if not param_schema then
        return false, "Invalid parameter schema"
    end
    --TODO: handle anyOf where type is nil
    if not param_schema.type then
        return true
    end

    -- Get type handler
    local handler = Handlers.TypeHandlers[param_schema.type]
    if not handler then
        return false, "Unknown parameter type: " .. param_schema.type
    end

    -- Validate value
    local is_valid = handler.validate(value, param_schema)
    if not is_valid then
        return false, string.format("Invalid %s value: %s", param_schema.type, value)
    end

    return true, nil
end

function ToolHandler:convert_param(name, value)
    local param_schema = self.parsedInputSchema.properties[name]
    local handler = Handlers.TypeHandlers[param_schema.type]
    if not handler then
        return value
    end
    return handler.convert(value)
end

function ToolHandler:format_param_type(param)
    local handler = Handlers.TypeHandlers[param.type]
    if not handler then
        return param.type
    end
    return handler.format(param)
end

function ToolHandler:validate_all_params()
    if not self.parsedInputSchema then
        return true, nil, {}
    end

    local errors = {}
    local params = self:get_ordered_params(self.parsedInputSchema)

    for _, param in ipairs(params) do
        local value = self.state.params.values[param.name]

        -- Check required fields
        if param.required and (not value or value == "") then
            errors[param.name] = "Required parameter"
        -- Only validate non-empty values
        elseif value and value ~= "" then
            local ok, err = self:validate_param(param.name, value)
            if not ok then
                errors[param.name] = err
            end
        end
        -- Skip validation for empty optional fields
    end

    if next(errors) then
        return false, "Some required parameters are missing or invalid", errors
    end

    return true, nil, {}
end

-- Action handling
-- Common callback logic for input handling
function ToolHandler:handle_param_update(param_name, input)
    -- Clear previous error
    self.state.params.errors[param_name] = nil

    -- Handle empty input
    if input == "" then
        -- Check if field is required
        local is_required = vim.tbl_contains(self.parsedInputSchema.required or {}, param_name)
        if is_required then
            self.state.params.errors[param_name] = "Required parameter"
        else
            -- For optional fields, clear value and error
            self.state.params.values[param_name] = nil
        end
    else
        -- Only validate non-empty input
        local ok, err = self:validate_param(param_name, input)
        if not ok then
            self.state.params.errors[param_name] = err
        else
            -- Update value
            self.state.params.values[param_name] = input
        end
    end
    self.view:draw()
end

function ToolHandler:handle_input_action(param_name)
    local param_schema = self.parsedInputSchema.properties[param_name]
    if not param_schema then
        return
    end

    self:handle_input(
        string.format("%s (%s): ", param_name, self:format_param_type(param_schema)),
        self.state.params.values[param_name],
        function(input)
            self:handle_param_update(param_name, input)
        end
    )
end

function ToolHandler:handle_text_box(line)
    local type, context = self:get_line_info(line)
    if type == "input" then
        local param_name = context
        local param_schema = self.parsedInputSchema.properties[param_name]
        if not param_schema then
            return
        end

        self:open_text_box(
            string.format("%s (%s)", param_name, self:format_param_type(param_schema)),
            self.state.params.values[param_name] or "",
            function(input)
                self:handle_param_update(param_name, input)
            end
        )
    end
end

function ToolHandler:handle_action(line)
    local type, context = self:get_line_info(line)
    if not type then
        return
    end

    if type == "input" then
        self:handle_input_action(context)
    elseif type == "submit" then
        self:execute()
    end
end

-- Execution
function ToolHandler:execute()
    -- Check if already executing
    if self.state.is_executing then
        vim.notify("Tool is already running", vim.log.levels.WARN)
        return
    end

    -- Validate all parameters first
    local ok, err, errors = self:validate_all_params()
    self.state.params.errors = errors
    self.state.error = err
    if not ok then
        self.view:draw()
        return
    end

    -- Set executing state
    self.state.is_executing = true
    self.state.error = nil
    self.view:draw()

    -- Convert all parameters to their proper types
    local converted_values = {}
    for name, value in pairs(self.state.params.values) do
        converted_values[name] = self:convert_param(name, value)
    end

    log.debug(string.format("Executing tool %s with parameters: %s", self.def.name, vim.inspect(converted_values)))
    -- Execute tool
    if State.hub_instance then
        State.hub_instance:call_tool(self.server_name, self.def.name, converted_values, {
            caller = {
                type = "hubui",
                hubui = State.ui_instance,
            },
            parse_response = true,
            callback = function(response, error)
                self:handle_response(response, error)
                self.view:draw()
            end,
        })
    end
end

function ToolHandler:render_submit()
    local submit_content
    if self.state.is_executing then
        submit_content = NuiLine():append("[ " .. Text.icons.event .. " Processing... ]", highlights.muted)
    else
        submit_content = NuiLine():append("[ " .. Text.icons.gear .. " Submit ]", highlights.success_fill)
    end
    return submit_content
end

-- Rendering
function ToolHandler:render_param_form(line_offset)
    -- Clear previous line tracking
    self:clear_line_tracking()

    local lines = {}

    local inputSchema = self.def.inputSchema
    local is_function = type(inputSchema) == "function"
    -- Parameters section
    vim.list_extend(
        lines,
        self:render_section_start((is_function and "(" .. Text.icons.event .. " Dynamic) " or "") .. "Input Params")
    )
    inputSchema = self:get_inputSchema(inputSchema)
    self.parsedInputSchema = inputSchema
    if not inputSchema or not next(inputSchema.properties or {}) then
        -- No parameters case
        local placeholder = NuiLine():append("No parameters required ", highlights.muted)

        -- Submit button
        local submit_content = self:render_submit()
        vim.list_extend(
            lines,
            self:render_section_content({ placeholder, NuiLine():append(" ", highlights.muted), submit_content }, 2)
        )

        -- Track submit line
        self:track_line(line_offset + #lines, "submit")
    else
        -- Render each parameter
        local params = self:get_ordered_params(inputSchema)
        for _, param in ipairs(params) do
            -- Parameter name and type
            local name_line = NuiLine()
                :append(param.required and "* " or "  ", highlights.error)
                :append(param.name, highlights.success)
                :append(string.format(" (%s)", self:format_param_type(param)), highlights.muted)
            vim.list_extend(lines, self:render_section_content({ name_line }, 2))

            -- Description if any
            if param.description then
                for _, desc_line in ipairs(Text.multiline(param.description, highlights.muted)) do
                    vim.list_extend(lines, self:render_section_content({ desc_line }, 4))
                end
            end

            -- Input field
            local value = self.state.params.values[param.name]
            local input_line = NuiLine():append("> ", highlights.success):append(value or "", highlights.info)
            vim.list_extend(lines, self:render_section_content({ input_line }, 2))

            -- Track input line
            self:track_line(line_offset + #lines, "input", param.name)

            -- Error if any
            if self.state.params.errors[param.name] then
                local error_lines = Text.multiline(self.state.params.errors[param.name], highlights.error)
                vim.list_extend(lines, self:render_section_content(error_lines, 2))
            end

            table.insert(lines, Text.pad_line(NuiLine():append("│", highlights.muted)))
        end

        -- Submit button
        local submit_content = self:render_submit()
        vim.list_extend(lines, self:render_section_content({ submit_content }, 2))

        -- Track submit line
        self:track_line(line_offset + #lines, "submit")
    end

    -- Error message
    if self.state.error then
        local error_lines = Text.multiline(self.state.error, highlights.error)
        vim.list_extend(lines, self:render_section_content(error_lines, 2))
    end
    vim.list_extend(lines, self:render_section_end())
    return lines
end

function ToolHandler:get_inputSchema(inputSchema)
    local base = {
        type = "object",
        properties = {},
    }
    inputSchema = self.def.inputSchema
    if not inputSchema or (type(inputSchema) ~= "function" and not next(inputSchema or {})) then
        inputSchema = base
    end
    local parsedSchema = inputSchema
    if type(inputSchema) == "function" then
        local ok, schema = pcall(inputSchema, self.def)
        if not ok then
            local err = "Error in inputSchema function: " .. tostring(schema)
            self.state.error = err
            log.error(err)
            parsedSchema = base
        else
            parsedSchema = schema or base
        end
    end
    local res = validation.validate_inputSchema(parsedSchema, self.def.name)
    if not res.ok then
        local err = "Error in inputSchema function: " .. tostring(res.error)
        self.state.error = err
        log.error(err)
        return base
    end
    return parsedSchema
end

function ToolHandler:render(line_offset)
    line_offset = line_offset or 0
    local lines = {}
    vim.list_extend(lines, vim.tbl_map(Text.pad_line, Text.multiline(self:get_description(), highlights.muted)))
    table.insert(lines, Text.pad_line(NuiLine()))
    -- Parameter form
    vim.list_extend(lines, self:render_param_form(line_offset + #lines))

    -- Result if any
    vim.list_extend(lines, self:render_result())

    return lines
end

return ToolHandler

local Base = require("mcphub.ui.capabilities.base")
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local highlights = require("mcphub.utils.highlights").groups

---@class ResourceTemplateHandler : CapabilityHandler
local ResourceTemplateHandler = setmetatable({}, {
    __index = Base,
})
ResourceTemplateHandler.__index = ResourceTemplateHandler
ResourceTemplateHandler.type = "resourceTemplate"

function ResourceTemplateHandler:new(server_name, capability_info, view)
    local handler = Base.new(self, server_name, capability_info, view)
    handler.state = vim.tbl_extend("force", handler.state, {
        input_value = "",
    })
    return handler
end

function ResourceTemplateHandler:execute()
    -- Check if already executing
    if self.state.is_executing then
        vim.notify("Resource template access is already in progress", vim.log.levels.WARN)
        return
    end
    if not self.state.input_value or vim.trim(self.state.input_value or "") == "" then
        self.state.error = "URI is required"
        self.view:draw()
        return
    end

    -- Set executing state
    self.state.is_executing = true
    self.state.error = nil
    self.view:draw()

    -- Access resource with user provided URI
    if State.hub_instance then
        State.hub_instance:access_resource(self.server_name, self.state.input_value, {
            caller = {
                type = "hubui",
                hubui = State.ui_instance,
            },
            parse_response = true,
            callback = function(response, err)
                self:handle_response(response, err)
                self.view:draw()
            end,
        })
    end
end

function ResourceTemplateHandler:handle_input_update(input)
    -- Update value
    self.state.input_value = input
    self.view:draw()
end

function ResourceTemplateHandler:handle_input_action()
    self:handle_input(string.format("URI: "), self.state.input_value, function(input)
        self:handle_input_update(input)
    end)
end

function ResourceTemplateHandler:handle_text_box(line)
    local type = self:get_line_info(line)
    if type == "input" then
        self:open_text_box("Resource Template URI", self.state.input_value or "", function(input)
            self:handle_input_update(input)
        end)
    end
end

function ResourceTemplateHandler:handle_action(line)
    local type = self:get_line_info(line)
    if type == "submit" then
        self:execute()
    elseif type == "input" then
        self:handle_input_action()
    end
end

function ResourceTemplateHandler:render(line_offset)
    line_offset = line_offset or 0
    -- Clear previous line tracking
    self:clear_line_tracking()

    local lines = {}

    -- Resource template info section
    vim.list_extend(lines, self:render_section_start("Template Information"))

    -- Template details
    local details = {
        NuiLine()
            :append("Name: ", highlights.muted)
            :append(self.def.name or "N/A", self.def.name and highlights.success or highlights.muted),
        NuiLine():append("Template: ", highlights.muted):append(self.def.uriTemplate, highlights.info),
    }

    if self.def.mimeType then
        table.insert(details, NuiLine():append("Type: ", highlights.muted):append(self.def.mimeType, highlights.info))
    end

    vim.list_extend(lines, self:render_section_content(details, 2))

    table.insert(lines, Text.pad_line(NuiLine():append("│", highlights.muted)))
    vim.list_extend(lines, self:render_section_content(Text.multiline(self:get_description(), highlights.muted), 2))

    vim.list_extend(lines, self:render_section_end())

    -- Action section
    table.insert(lines, Text.pad_line(NuiLine()))
    vim.list_extend(lines, self:render_section_start("Access Resource"))

    -- Input field
    local input_line = NuiLine():append("> ", highlights.success):append(self.state.input_value or "", highlights.info)
    vim.list_extend(lines, self:render_section_content({ input_line }, 2))

    -- Track input line
    self:track_line(line_offset + #lines, "input")

    -- Action button
    local button_line
    if self.state.is_executing then
        button_line = NuiLine()
            :append("[ ", highlights.muted)
            :append("Processing...", highlights.muted)
            :append(" ]", highlights.muted)
    else
        button_line = NuiLine()
            :append("[ ", highlights.success_fill)
            :append("Access", highlights.success_fill)
            :append(" ]", highlights.success_fill)
    end
    vim.list_extend(lines, self:render_section_content({ NuiLine():append(" "), button_line }, 2))

    -- Track submit line for interaction
    self:track_line(line_offset + #lines, "submit")

    -- Error message if any
    if self.state.error then
        table.insert(lines, Text.pad_line(NuiLine():append("│", highlights.muted)))
        local error_lines = Text.multiline(self.state.error, highlights.error)
        vim.list_extend(lines, self:render_section_content(error_lines, 2))
    end

    vim.list_extend(lines, self:render_section_end())

    -- Result section if any
    vim.list_extend(lines, self:render_result())

    return lines
end

return ResourceTemplateHandler

local Base = require("mcphub.ui.capabilities.base")
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local highlights = require("mcphub.utils.highlights").groups

---@class ResourceHandler : CapabilityHandler
local ResourceHandler = setmetatable({}, {
    __index = Base,
})
ResourceHandler.__index = ResourceHandler
ResourceHandler.type = "resource"

function ResourceHandler:new(server_name, capability_info, view)
    local handler = Base.new(self, server_name, capability_info, view)
    return handler
end

function ResourceHandler:execute()
    -- Check if already executing
    if self.state.is_executing then
        vim.notify("Resource access is already in progress", vim.log.levels.WARN)
        return
    end

    -- Set executing state
    self.state.is_executing = true
    self.state.error = nil
    self.view:draw()

    -- Access resource
    if State.hub_instance then
        State.hub_instance:access_resource(self.server_name, self.def.uri, {
            caller = {
                type = "hubui",
                hubui = State.ui_instance,
            },
            parse_response = true,
            callback = function(response, err)
                self:handle_response(response, err)
                self.view:draw()
            end,
        })
    end
end

function ResourceHandler:handle_action(line)
    local type = self:get_line_info(line)
    if type == "submit" then
        self:execute()
    end
end

function ResourceHandler:render(line_offset)
    line_offset = line_offset or 0
    -- Clear previous line tracking
    self:clear_line_tracking()

    local lines = {}

    -- Resource info section
    vim.list_extend(lines, self:render_section_start("Resource Information"))

    -- Resource details
    local details = {
        NuiLine()
            :append("Name: ", highlights.muted)
            :append(self.def.name or "N/A", self.def.name and highlights.success or highlights.muted),
        NuiLine():append("Type: ", highlights.muted):append(self.def.mimeType or "unknown", highlights.info),
        NuiLine():append("URI: ", highlights.muted):append(self.def.uri, highlights.link),
    }

    vim.list_extend(lines, self:render_section_content(details, 2))
    table.insert(lines, Text.pad_line(NuiLine():append("│", highlights.muted)))
    vim.list_extend(lines, self:render_section_content(Text.multiline(self:get_description(), highlights.muted), 2))
    vim.list_extend(lines, self:render_section_end())

    -- Action section
    table.insert(lines, Text.pad_line(NuiLine()))
    vim.list_extend(lines, self:render_section_start("Access Resource"))

    -- Action button
    local button_line
    if self.state.is_executing then
        button_line = NuiLine()
            :append("[ ", highlights.muted)
            :append("Processing...", highlights.muted)
            :append(" ]", highlights.muted)
    else
        button_line = NuiLine()
            :append("[ ", highlights.success_fill)
            :append("Access", highlights.success_fill)
            :append(" ]", highlights.success_fill)
    end
    vim.list_extend(lines, self:render_section_content({ NuiLine():append(" "), button_line }, 2))

    -- Track submit line for interaction
    self:track_line(line_offset + #lines, "submit")

    -- Error message if any
    if self.state.error then
        table.insert(lines, Text.pad_line(NuiLine():append("│", highlights.muted)))
        local error_lines = Text.multiline(self.state.error, highlights.error)
        vim.list_extend(lines, self:render_section_content(error_lines, 2))
    end

    vim.list_extend(lines, self:render_section_end())

    -- Result section if any
    vim.list_extend(lines, self:render_result())

    return lines
end

return ResourceHandler

local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local config = require("mcphub.config")
local ui_utils = require("mcphub.utils.ui")
local validation = require("mcphub.utils.validation")

local M = {}

--- Clean command arguments by filtering out empty strings and nil values.
--- This is particularly useful when handling command arguments that may contain optional values.
--- @param args table Array of command arguments
--- @return table Cleaned array with only valid arguments
function M.clean_args(args)
    return vim.iter(args or {})
        :flatten()
        :filter(function(arg)
            return arg ~= "" and arg ~= nil
        end)
        :totable()
end

local uid = 0
function M.gen_dump_path()
    uid = uid + 1
    local P = require("plenary.path")
    local path
    local id = string.gsub("xxxx4xxx", "[xy]", function(l)
        local v = (l == "x") and math.random(0, 0xf) or math.random(0, 0xb)
        return string.format("%x", v)
    end)
    if P.path.sep == "\\" then
        path = string.format("%s\\AppData\\Local\\Temp\\plenary_curl_%s.headers", os.getenv("USERPROFILE"), id)
    else
        local temp_dir = os.getenv("XDG_RUNTIME_DIR") or "/tmp"
        path = temp_dir .. "/plenary_curl_" .. id .. ".headers"
    end
    local nvim_pid = vim.uv.os_getpid()
    local dump_file = path .. nvim_pid .. uid
    return { "-D", dump_file .. ".headers" }
end

--- Format timestamp relative to now
---@param timestamp number Unix timestamp
---@return string
function M.format_relative_time(timestamp)
    local now = vim.loop.now()
    local diff = math.floor(now - timestamp)

    if diff < 1000 then -- Less than a second
        return "just now"
    elseif diff < 60000 then -- Less than a minute
        local seconds = math.floor(diff / 1000)
        return string.format("%ds", seconds)
    elseif diff < 3600000 then -- Less than an hour
        local minutes = math.floor(diff / 60000)
        local seconds = math.floor((diff % 60000) / 1000)
        return string.format("%dm %ds", minutes, seconds)
    elseif diff < 86400000 then -- Less than a day
        local hours = math.floor(diff / 3600000)
        local minutes = math.floor((diff % 3600000) / 60000)
        return string.format("%dh %dm", hours, minutes)
    else -- Days
        local days = math.floor(diff / 86400000)
        local hours = math.floor((diff % 86400000) / 3600000)
        return string.format("%dd %dh", days, hours)
    end
end

--- Format duration in seconds to human readable string
---@param seconds number Duration in seconds
---@return string Formatted duration
function M.format_uptime(seconds)
    if seconds < 60 then
        return string.format("%ds", seconds)
    elseif seconds < 3600 then
        return string.format("%dm %ds", math.floor(seconds / 60), seconds % 60)
    else
        local hours = math.floor(seconds / 3600)
        local mins = math.floor((seconds % 3600) / 60)
        return string.format("%dh %dm", hours, mins)
    end
end

--- Calculate the approximate number of tokens in a text string
--- This is a simple approximation using word count, which works reasonably well for most cases
---@param text string The text to count tokens from
---@return number approx_tokens The approximate number of tokens
function M.calculate_tokens(text)
    if not text or text == "" then
        return 0
    end

    -- Simple tokenization approximation (4 chars ≈ 1 token)
    local char_count = #text
    local approx_tokens = math.ceil(char_count / 4)

    -- Alternative method using word count
    -- local words = {}
    -- for word in text:gmatch("%S+") do
    --     table.insert(words, word)
    -- end
    -- local word_count = #words
    -- local approx_tokens = math.ceil(word_count * 1.3) -- Words + punctuation overhead

    return approx_tokens
end

--- Format token count for display
---@param count number The token count
---@return string formatted The formatted token count
function M.format_token_count(count)
    if count < 1000 then
        return tostring(count)
    elseif count < 1000000 then
        return string.format("%.1fk", count / 1000)
    else
        return string.format("%.1fM", count / 1000000)
    end
end

--- Fire an autocommand event with data
---@param name string The event name (without "User" prefix)
---@param data? table Optional data to pass to the event
function M.fire(name, data)
    vim.api.nvim_exec_autocmds("User", {
        pattern = name,
        data = data,
    })
end

--- Sort table keys recursively while preserving arrays
---@param tbl table The table to sort
---@return table sorted_tbl The sorted table
local function sort_keys_recursive(tbl)
    if type(tbl) ~= "table" then
        return tbl
    end

    -- Check if table is an array
    local is_array = true
    local max_index = 0
    for k, _ in pairs(tbl) do
        if type(k) ~= "number" then
            is_array = false
            break
        end
        max_index = math.max(max_index, k)
    end
    if is_array and max_index == #tbl then
        -- Process array values but preserve order
        local result = {}
        for i, v in ipairs(tbl) do
            result[i] = sort_keys_recursive(v)
        end
        return result
    end

    -- Sort object keys alphabetically (case-insensitive)
    local sorted = {}
    local keys = {}

    for k in pairs(tbl) do
        table.insert(keys, k)
    end
    table.sort(keys)
    for _, k in ipairs(keys) do
        sorted[k] = sort_keys_recursive(tbl[k])
    end
    return sorted
end

local _is_jq_available
local function is_jq_available()
    if _is_jq_available ~= nil then
        return _is_jq_available
    end
    _is_jq_available = vim.fn.executable("jq") == 1
    return _is_jq_available
end

--- Pretty print JSON string with optional unescaping of forward slashes
---@param str string JSON string to format
---@param opts? { unescape_slashes?: boolean, use_jq?: boolean, sort_keys?: boolean } Optional options
---@return string Formatted JSON string
function M.pretty_json(str, opts)
    opts = opts or {}

    if opts.use_jq and is_jq_available() then
        local jq_cmd = { "jq" }
        if opts.sort_keys ~= false then
            vim.list_extend(jq_cmd, { "--sort-keys", "." })
        end
        local formatted = vim.fn.system(jq_cmd, str)
        if vim.v.shell_error ~= 0 or not formatted or formatted == "" then
            return M.format_json_string(str, opts.unescape_slashes)
        end
        if opts.unescape_slashes == nil or opts.unescape_slashes then
            formatted = formatted:gsub("\\/", "/")
        end
        return formatted
    else
        -- Fallback to custom implementation
        local ok, parsed = M.json_decode(str)
        if not ok then
            vim.notify("Failed to parse JSON string", vim.log.levels.INFO)
            return M.format_json_string(str, opts.unescape_slashes)
        end
        local sorted = opts.sort_keys ~= false and sort_keys_recursive(parsed) or parsed
        local encoded = vim.json.encode(sorted)
        return M.format_json_string(encoded, opts.unescape_slashes)
    end
end

--- Format a JSON string with proper indentation
---@param str string JSON string to format
---@return string Formatted JSON string
function M.format_json_string(str, unescape_slashes)
    local level = 0
    local result = ""
    local in_quotes = false
    local escape_next = false
    local indent = "  "
    -- Default to true if not specified
    if unescape_slashes == nil then
        unescape_slashes = true
    end

    -- Pre-process to unescape forward slashes if requested
    if unescape_slashes then
        str = str:gsub("\\/", "/")
    end

    for i = 1, #str do
        local char = str:sub(i, i)

        -- Handle escape sequences properly
        if escape_next then
            escape_next = false
            result = result .. char
        elseif char == "\\" then
            escape_next = true
            result = result .. char
        elseif char == '"' then
            in_quotes = not in_quotes
            result = result .. char
        elseif not in_quotes then
            if char == "{" or char == "[" then
                level = level + 1
                result = result .. char .. "\n" .. string.rep(indent, level)
            elseif char == "}" or char == "]" then
                level = level - 1
                result = result .. "\n" .. string.rep(indent, level) .. char
            elseif char == "," then
                result = result .. char .. "\n" .. string.rep(indent, level)
            elseif char == ":" then
                -- Add space after colons for readability
                result = result .. ": "
            elseif char == " " or char == "\n" or char == "\t" then
            -- Skip whitespace in non-quoted sections
            -- (vim.json.encode already adds its own whitespace)
            else
                result = result .. char
            end
        else
            -- In quotes, preserve all characters
            result = result .. char
        end
    end
    return result
end

function M.is_windows()
    return vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1
end

---@param config MCPHub.Config
---@return { cmd: string, cmdArgs: string[] }
function M.get_default_cmds(config)
    local cmd, cmdArgs
    local bin_name = nil
    if config.use_bundled_binary then
        bin_name = M.get_bundled_mcp_path()
    else
        if M.is_windows() then
            bin_name = "mcp-hub.cmd"
        else
            bin_name = "mcp-hub"
        end
    end
    -- set default cmds
    if config.cmd == nil and config.cmdArgs == nil then
        if M.is_windows() then
            cmd = "cmd.exe"
            cmdArgs = {
                "/C",
                bin_name,
            }
        else
            cmd = bin_name
            cmdArgs = {}
        end
    else
        cmd = config.cmd or bin_name
        cmdArgs = config.cmdArgs or {}
    end
    return {
        cmd = cmd,
        cmdArgs = cmdArgs,
    }
end

--- Get path to bundled mcp-hub executable when build = "bundled_build.lua"
---@return string Path to mcp-hub executable in bundled directory
function M.get_bundled_mcp_path()
    local plugin_root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":h:h:h:h")
    local base_path = plugin_root .. "/bundled/mcp-hub/node_modules/.bin/mcp-hub"
    if M.is_windows() then
        return base_path .. ".cmd"
    end
    return base_path
end

function M.safe_get(tbl, path)
    -- Handle nil input
    if tbl == nil then
        return nil
    end

    -- Split path by dots
    local parts = {}
    for part in path:gmatch("[^.]+") do
        parts[#parts + 1] = part
    end

    local current = tbl
    for _, key in ipairs(parts) do
        -- Convert string numbers to numeric indices
        if tonumber(key) then
            key = tonumber(key)
        end

        if type(current) ~= "table" then
            return nil
        end
        current = current[key]
        if current == nil then
            return nil
        end
    end

    return current
end

function M.parse_context(caller)
    local bufnr = nil
    local context = {}
    local type = caller.type
    local meta = caller.meta or {}
    if type == "codecompanion" then
        local is_within_variable = meta.is_within_variable == true
        local chat
        if is_within_variable then
            chat = M.safe_get(caller, "codecompanion.Chat") or M.safe_get(caller, "codecompanion.inline")
        else
            chat = M.safe_get(caller, "codecompanion.chat")
        end
        bufnr = M.safe_get(chat, "context.bufnr") or 0
    elseif type == "avante" then
        bufnr = M.safe_get(caller, "avante.code.bufnr") or 0
    elseif type == "hubui" then
        context = M.safe_get(caller, "hubui.context") or {}
    end
    return vim.tbl_extend("force", {
        bufnr = bufnr,
    }, context)
end

---@param mode string
---@return boolean
local function is_visual_mode(mode)
    return mode == "v" or mode == "V" or mode == "^V"
end

---Get the context of the current buffer.
---@param bufnr? integer
---@param args? table
---@return table
function M.get_buf_info(bufnr, args)
    bufnr = bufnr or vim.api.nvim_get_current_buf()

    -- Validate buffer
    if not vim.api.nvim_buf_is_valid(bufnr) then
        error("Invalid buffer number: " .. tostring(bufnr))
    end

    -- Find the window displaying this buffer
    local winnr
    for _, win in ipairs(vim.api.nvim_list_wins()) do
        if vim.api.nvim_win_get_buf(win) == bufnr then
            winnr = win
            break
        end
    end

    -- Fallback to current window if buffer isn't displayed
    if not winnr then
        winnr = vim.api.nvim_get_current_win()
    end
    local mode = vim.fn.mode()
    local cursor_pos = { 1, 0 } -- Default to start of buffer

    -- Only get cursor position if we have a valid window
    if winnr and vim.api.nvim_win_is_valid(winnr) then
        local ok, pos = pcall(vim.api.nvim_win_get_cursor, winnr)
        if ok then
            cursor_pos = pos
        end
    end

    -- Get all buffer lines for context
    local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
    local start_line = cursor_pos[1]
    local start_col = cursor_pos[2]
    local end_line = cursor_pos[1]
    local end_col = cursor_pos[2]

    local is_visual = false
    local is_normal = true

    local function try_get_visual_selection()
        local ok, result = pcall(function()
            if args and args.range and args.range > 0 then
                is_visual = true
                is_normal = false
                mode = "v"
                return M.get_visual_selection(bufnr)
            elseif is_visual_mode(mode) then
                is_visual = true
                is_normal = false
                return M.get_visual_selection(bufnr)
            end
            return lines, start_line, start_col, end_line, end_col
        end)

        if not ok then
            -- Fallback to current cursor position on error
            vim.notify("Failed to get visual selection: " .. tostring(result), vim.log.levels.WARN)
            is_visual = false
            is_normal = true
            return lines, start_line, start_col, end_line, end_col
        end
        return result
    end

    lines, start_line, start_col, end_line, end_col = try_get_visual_selection()

    return {
        winnr = winnr,
        bufnr = bufnr,
        mode = mode,
        is_visual = is_visual,
        is_normal = is_normal,
        buftype = vim.api.nvim_buf_get_option(bufnr, "buftype") or "",
        filetype = vim.api.nvim_buf_get_option(bufnr, "filetype") or "",
        filename = vim.api.nvim_buf_get_name(bufnr),
        cursor_pos = cursor_pos,
        lines = lines,
        line_count = vim.api.nvim_buf_line_count(bufnr),
        start_line = start_line,
        start_col = start_col,
        end_line = end_line,
        end_col = end_col,
    }
end

---@class ConfigParseResult
---@field ok boolean
---@field error string|nil
---@field name string|nil
---@field config table|nil

---@param text string
---@return ConfigParseResult
function M.parse_config_from_json(text)
    local result = {
        ok = false,
        error = nil,
        name = nil,
        config = nil,
    }

    local ok, parsed = M.json_decode(text)
    if not ok then
        result.error = "Invalid JSON format"
        return result
    end

    -- Case 1: Full mcpServers object
    if parsed.mcpServers then
        local name, config = next(parsed.mcpServers)
        if not name then
            result.error = "No server config found in mcpServers"
            return result
        end
        result.ok = true
        result.name = name
        result.config = config
        return result
    end

    -- Case 2: Server config object
    if parsed.command or parsed.url then
        local is_string = parsed.command and type(parsed.command) == "string" or type(parsed.url) == "string"
        if is_string then
            result.ok = true
            result.name = "unnamed"
            result.config = parsed
            return result
        end
    end

    -- Case 3: Single server name:config pair
    if vim.tbl_count(parsed) == 1 then
        local name, config = next(parsed)
        result.ok = true
        result.name = name
        result.config = config
        return result
    end

    result.error = "JSON should have a mcpServers key or name:config pair"
    return result
end

---@param opts { title: string?, placeholder: string?, old_server_name: string?, start_insert: boolean?, is_native: boolean?, on_success: function?, on_error: function?, go_to_placeholder: boolean?, virtual_lines: Array[]?, config_source: string?, ask_for_source: boolean? }
function M.open_server_editor(opts)
    opts = opts or {}
    ui_utils.multiline_input(opts.title or "Paste server's JSON config", opts.placeholder or "", function(content)
        if not content or vim.trim(content) == "" then
            if opts.on_error then
                opts.on_error("No content provided")
            end
            return
        end
        local result = M.parse_config_from_json(content)
        if result.ok then
            if opts.old_server_name and opts.old_server_name ~= "" then
                if result.name ~= opts.old_server_name then
                    if opts.is_native then
                        if opts.on_error then
                            opts.on_error("Server name cannot be changed for native servers")
                        else
                            vim.notify("Server name cannot be changed for native servers", vim.log.levels.ERROR)
                        end
                        return
                    end
                    -- If an old server name is provided, remove the old config
                    State.hub_instance:remove_server_config(opts.old_server_name)
                    vim.notify("Server " .. opts.old_server_name .. " removed", vim.log.levels.INFO)
                end
            end

            local function save(config_source)
                local success = State.hub_instance:update_server_config(
                    result.name,
                    result.config,
                    { merge = false, config_source = config_source }
                )
                if success then
                    vim.notify("Server " .. result.name .. " added successfully", vim.log.levels.INFO)
                    if opts.on_success then
                        opts.on_success(result.name, result.config)
                    end
                else
                    local error_msg = "Failed to update server configuration"
                    if opts.on_error then
                        opts.on_error(error_msg)
                    else
                        vim.notify(error_msg, vim.log.levels.ERROR)
                    end
                end
            end
            local config_files = State.current_hub.config_files or {}
            if opts.ask_for_source and #config_files > 1 then
                vim.ui.select(config_files, {
                    prompt = "Select config path to save the server configuration",
                }, function(choice)
                    if choice then
                        save(choice)
                    else
                        if opts.on_error then
                            opts.on_error("No config source selected")
                        end
                        return
                    end
                end)
            else
                save(opts.config_source)
            end
        else
            if opts.on_error then
                opts.on_error(result.error)
            else
                vim.notify(result.error, vim.log.levels.ERROR)
            end
        end
    end, {
        filetype = "json",
        start_insert = opts.start_insert or false,
        show_footer = false,
        position = "center", -- Always use center positioning for server editor
        go_to_placeholder = opts.go_to_placeholder,
        virtual_lines = opts.virtual_lines,
        validate = function(content)
            local result = M.parse_config_from_json(content)
            if not result.ok then
                vim.notify(result.error, vim.log.levels.ERROR)
                return false
            end

            if opts.is_native then
                if type(result.config) ~= "table" then
                    vim.notify("Config must be a table", vim.log.levels.ERROR)
                    return false
                end
                -- Native servers only need basic config validation
                return true
            else
                local valid = validation.validate_server_config(result.name, result.config)
                if not valid.ok then
                    vim.notify(valid.error.message, vim.log.levels.ERROR)
                    return false
                end
            end
            return true
        end,
        on_cancel = function()
            if opts.on_error then
                opts.on_error("User cancelled")
            end
        end,
    })
end

--- Confirm and delete a server configuration with a detailed preview
--- @param server_name string The name of the server to delete
--- @param on_delete function? Callback function to call after deletion
function M.confirm_and_delete_server(server_name, on_delete)
    if not server_name or server_name == "" then
        vim.notify("Server name is required for deletion", vim.log.levels.ERROR)
        return
    end
    local async = require("plenary.async")
    async.run(function()
        local config_manager = require("mcphub.utils.config_manager")
        local server_config = config_manager.get_server_config(server_name, true) or {}
        -- Create confirmation message with highlights
        local message = {}

        -- Header line with highlights
        local header_line = NuiLine()
        header_line:append("Do you want to delete ", Text.highlights.muted)
        header_line:append("'" .. server_name .. "'", Text.highlights.error)
        header_line:append(" server?", Text.highlights.muted)
        table.insert(message, header_line)
        table.insert(message, "")

        -- Current configuration preview
        if server_config then
            vim.list_extend(message, Text.render_json(vim.json.encode(server_config)))
        else
            local no_config_line = NuiLine()
            no_config_line:append("No current configuration found.", Text.highlights.muted)
            table.insert(message, no_config_line)
        end

        -- Show confirmation dialog
        local confirmed, cancelled = ui_utils.confirm(message, {
            min_width = 70,
            max_width = 90,
        })

        if confirmed and not cancelled then
            -- Delete the server
            if State.hub_instance then
                local success = State.hub_instance:remove_server_config(server_name)
                if success then
                    vim.notify("Server '" .. server_name .. "' deleted successfully!", vim.log.levels.INFO)
                    if on_delete then
                        on_delete(server_name)
                    end
                else
                    vim.notify("Failed to delete server '" .. server_name .. "'", vim.log.levels.ERROR)
                end
            else
                vim.notify("MCP Hub not available", vim.log.levels.ERROR)
            end
        end
    end, function() end)
end

--- Convert ISO 8601 timestamp to relative time format (e.g., "23s", "1d", "1hr", "2m")
---@param iso_string string ISO 8601 timestamp string
---@return string Relative time string
function M.iso_to_relative_time(iso_string)
    if not iso_string or iso_string == "" then
        return "unknown"
    end

    -- Parse ISO string to Unix timestamp
    local pattern = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)"
    local year, month, day, hour, min, sec = iso_string:match(pattern)

    if not year then
        return "invalid"
    end

    -- Convert to Unix timestamp
    local timestamp = os.time({
        year = tonumber(year),
        month = tonumber(month),
        day = tonumber(day),
        hour = tonumber(hour),
        min = tonumber(min),
        sec = tonumber(sec),
    })

    -- Get current time
    local now = os.time()
    local diff = now - timestamp

    -- Handle future dates
    if diff < 0 then
        diff = math.abs(diff)
        local future_result = M._format_time_diff(diff)
        return "in " .. future_result
    end

    return M._format_time_diff(diff)
end

--- Convert ms to relative time format (e.g., "23s", "1d", "1hr", "2m")
--- @param ms number Time in milliseconds
--- @return string Relative time string
function M.ms_to_relative_time(ms)
    if type(ms) ~= "number" then
        ms = tonumber(ms)
    end
    if not ms or ms < 0 then
        return "unknown"
    end

    local seconds = math.floor(ms / 1000)
    return M._format_time_diff(seconds)
end

--- Internal helper to format time difference
---@param diff number Time difference in seconds
---@return string Formatted time string
function M._format_time_diff(diff)
    if diff < 60 then
        return string.format("%ds", diff)
    elseif diff < 3600 then -- Less than 1 hour
        return string.format("%dm", math.floor(diff / 60))
    elseif diff < 86400 then -- Less than 1 day
        return string.format("%dh", math.floor(diff / 3600))
    elseif diff < 2592000 then -- Less than 30 days
        return string.format("%dd", math.floor(diff / 86400))
    elseif diff < 31536000 then -- Less than 1 year
        return string.format("%dmo", math.floor(diff / 2592000))
    else
        return string.format("%dy", math.floor(diff / 31536000))
    end
end

--- Decode a JSON string into a Lua table
--- @param str string The JSON string to decode
--- @param opts? {use_custom_parser: boolean} Optional options
--- @return boolean The decoded Lua table, or nil if decoding fails
--- @return table|nil
function M.json_decode(str, opts)
    opts = opts or {}
    local ok, result

    -- Try decoding using vim.json.decode for all
    ok, result = pcall(vim.json.decode, str, { luanil = { object = true, array = true } })
    if ok then
        return ok, result
    end
    -- If decoding fails, check if we should use a custom parser
    if not ok and opts.use_custom_parser then
        -- Try custom parser first (lua-json5) e.g `require
        if type(config.json_decode) == "function" then
            ok, result = pcall(config.json_decode, str)
            if ok then
                return ok, result
            end
        end

        -- INFO: We can bundle a lightweight JSON5 parser using Node.js or rust but it needs a breaking change wrt the build step. Without build step, including the bundled script which is bundled in development might not go well with the users security concerns. So we will not include it for now and let the user provide a custom parser if needed.
        -- -- Fallback to bundled JSON5 parser
        -- local plugin_root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":h:h:h:h")
        -- local json5_script = plugin_root .. "/scripts/bundled_json5.js"
        --
        -- local json5_result = vim.fn.system({ "node", json5_script }, str)
        --
        -- if vim.v.shell_error == 0 and json5_result and vim.trim(json5_result) ~= "" then
        --     -- Parse the clean JSON output from bundled JSON5 parser
        --     ok, result = pcall(vim.json.decode, json5_result, { luanil = { object = true, array = true } })
        --     if ok then
        --         return ok, result
        --     end
        -- end
    end
    return false, result
end
return M

local M = {}
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local native = require("mcphub.native")
local ui_utils = require("mcphub.utils.ui")
local utils = require("mcphub.utils")

---@class MCPHub.ParsedParams
---@field errors string[] List of errors encountered during parsing
---@field action MCPHub.ActionType Action type, either "use_mcp_tool" or "access_mcp_resource"
---@field server_name string Name of the server to call the tool/resource on
---@field tool_name string Name of the tool to call (nil for resources)
---@field arguments table Input arguments for the tool call (empty table for resources)
---@field uri string URI of the resource to access (nil for tools)
---@field is_auto_approved_in_server boolean Whether the tool autoApproved in the servers.json
---@field needs_confirmation_window boolean Whether the tool call needs a confirmation window

---@class MCPHub.ToolCallArgs
---@field server_name string Name of the server to call the tool on.
---@field tool_name string Name of the tool to call.
---@field tool_input table | string Input for the tool call. Must be an object for `use_mcp_tool` action.

---@class MCPHub.ResourceAccessArgs
---@field server_name string Name of the server to call the resource on.
---@field uri string URI of the resource to access.

---@param server_name string Name of the server to check
---@param tool_name string Name of the tool to check
---@return boolean Whether the tool is auto-approved in the server
function M.is_auto_approved_in_server(server_name, tool_name)
    local config_manager = require("mcphub.utils.config_manager")
    local server_config = config_manager.get_server_config(server_name) or {}
    local auto_approve = server_config.autoApprove
    if not auto_approve then
        return false
    end
    -- If autoApprove is true, approve everything for this server
    if auto_approve == true then
        return true
    end
    -- If autoApprove is an array, check if tool is in the list
    if type(auto_approve) == "table" and vim.islist(auto_approve) then
        return vim.tbl_contains(auto_approve, tool_name)
    end
    return false
end

---@param params MCPHub.ToolCallArgs | MCPHub.ResourceAccessArgs
---@param action_name MCPHub.ActionType
---@return MCPHub.ParsedParams
function M.parse_params(params, action_name)
    params = params or {}

    local server_name = params.server_name
    local tool_name = params.tool_name
    local uri = params.uri
    local arguments = params.tool_input or {}
    if type(arguments) == "string" then
        local json_ok, decode_result = utils.json_decode(arguments or "{}")
        if json_ok then
            arguments = decode_result or {}
        else
            arguments = {}
        end
    end
    local errors = {}
    if not vim.tbl_contains({ "use_mcp_tool", "access_mcp_resource" }, action_name) then
        table.insert(errors, "Action must be one of `use_mcp_tool` or `access_mcp_resource`")
    end
    if not server_name then
        table.insert(errors, "server_name is required")
    end
    if action_name == "use_mcp_tool" and not tool_name then
        table.insert(errors, "tool_name is required")
    end
    if action_name == "use_mcp_tool" and type(arguments) ~= "table" then
        table.insert(errors, "tool_input must be an object")
    end

    if action_name == "access_mcp_resource" and not uri then
        table.insert(errors, "uri is required")
    end

    return {
        errors = errors,
        action = action_name or "nil",
        server_name = server_name or "nil",
        tool_name = tool_name or "nil",
        arguments = arguments or {},
        uri = uri or "nil",
        needs_confirmation_window = M.needs_confirmation_window(server_name, tool_name),
        is_auto_approved_in_server = M.is_auto_approved_in_server(server_name, tool_name),
    }
end

--- For some built-in tools, we already show interactive diffs, before confirmation.
---@param server_name string Name of the server
---@param tool_name string Name of the tool to check
function M.needs_confirmation_window(server_name, tool_name)
    local server = native.is_native_server(server_name)
    if not server then
        return true
    end
    for _, tool in ipairs(server.capabilities.tools) do
        if tool.name == tool_name and tool.needs_confirmation_window == false then
            return false
        end
    end
    return true
end
---@param arguments MCPPromptArgument[]
---@param callback fun(values: string[])
function M.collect_arguments(arguments, callback)
    local values = {}
    local should_proceed = true

    local function collect_input(index)
        if index > #arguments and should_proceed then
            callback(values)
            return
        end

        local arg = arguments[index]
        local title = string.format("%s %s", arg.name, arg.required and "(required)" or "")
        local default = arg.default or ""

        local function submit_input(input)
            if arg.required and (input == nil or input == "") then
                vim.notify("Value for " .. arg.name .. " is required", vim.log.levels.ERROR)
                should_proceed = false
                return
            end

            values[arg.name] = input
            collect_input(index + 1)
        end

        local function cancel_input()
            if arg.required then
                vim.notify("Value for " .. arg.name .. " is required", vim.log.levels.ERROR)
                should_proceed = false
                return
            end
            values[arg.name] = nil
            collect_input(index + 1)
        end
        ui_utils.multiline_input(title, default, submit_input, { on_cancel = cancel_input })
    end

    if #arguments > 0 then
        vim.defer_fn(function()
            collect_input(1)
        end, 0)
    else
        callback(values)
    end
end

---Create the confirmation prompt for mcp tool
---@param params MCPHub.ParsedParams
---@return string
function M.get_mcp_tool_prompt(params)
    local action_name = params.action
    local server_name = params.server_name
    local tool_name = params.tool_name
    local uri = params.uri
    local arguments = params.arguments or {}

    local args = ""
    for k, v in pairs(arguments) do
        args = args .. k .. ":\n "
        if type(v) == "string" then
            local lines = vim.split(v, "\n")
            for _, line in ipairs(lines) do
                args = args .. line .. "\n"
            end
        else
            args = args .. vim.inspect(v) .. "\n"
        end
    end
    local msg = ""
    if action_name == "use_mcp_tool" then
        msg = string.format(
            [[Do you want to run the `%s` tool on the `%s` mcp server with arguments:
%s]],
            tool_name,
            server_name,
            args
        )
    elseif action_name == "access_mcp_resource" then
        msg = string.format("Do you want to access the resource `%s` on the `%s` server?", uri, server_name)
    end
    return msg
end

---@param params MCPHub.ParsedParams
---@return boolean confirmed
---@return boolean cancelled
function M.show_mcp_tool_prompt(params)
    local action_name = params.action
    local server_name = params.server_name
    local tool_name = params.tool_name
    local uri = params.uri
    local arguments = params.arguments or {}

    local lines = {}
    local is_tool = action_name == "use_mcp_tool"

    -- Header as a question
    local header_line = NuiLine()
    header_line:append(Text.icons.event, Text.highlights.warn)
    header_line:append(" Do you want to ", Text.highlights.text)
    if is_tool then
        header_line:append("call ", Text.highlights.text)
        header_line:append(tool_name, Text.highlights.warn_italic)
    else
        header_line:append("access ", Text.highlights.text)
        header_line:append(uri, Text.highlights.link)
    end
    header_line:append(" on ", Text.highlights.text)
    header_line:append(server_name, Text.highlights.success_italic)
    header_line:append("?", Text.highlights.text)
    table.insert(lines, header_line)

    -- Parameters section
    if is_tool and next(arguments) then
        table.insert(lines, NuiLine():append(""))

        for key, value in pairs(arguments) do
            -- Parameter name
            local param_name_line = NuiLine()
            param_name_line:append(Text.icons.param, Text.highlights.info)
            param_name_line:append(" " .. key .. ":", Text.highlights.json_property)
            table.insert(lines, param_name_line)

            -- Parameter value
            local function add_value_lines(val)
                if type(val) == "string" then
                    local value_lines = val:find("\n") and vim.split(val, "\n", { plain = true })
                        or { '"' .. val .. '"' }
                    for _, line in ipairs(value_lines) do
                        local value_line = NuiLine()
                        value_line:append("    " .. line, Text.highlights.json_string)
                        table.insert(lines, value_line)
                    end
                elseif type(val) == "boolean" then
                    local value_line = NuiLine()
                    value_line:append("    " .. tostring(val), Text.highlights.json_boolean)
                    table.insert(lines, value_line)
                elseif type(val) == "number" then
                    local value_line = NuiLine()
                    value_line:append("    " .. tostring(val), Text.highlights.json_number)
                    table.insert(lines, value_line)
                else
                    for _, line in ipairs(vim.split(vim.inspect(val), "\n", { plain = true })) do
                        local value_line = NuiLine()
                        value_line:append("    " .. line, Text.highlights.muted)
                        table.insert(lines, value_line)
                    end
                end
            end

            add_value_lines(value)
            table.insert(lines, NuiLine():append(""))
        end
    end

    return require("mcphub.utils.ui").confirm(lines, {
        min_width = 70,
        max_width = 100,
    })
end

---@param parsed_params MCPHub.ParsedParams
---@return {error?:string, approve:boolean}
function M.handle_auto_approval_decision(parsed_params)
    local auto_approve = State.config.auto_approve or false
    local status = { approve = false, error = nil }
    --- If user has a custom function that decides whether to auto-approve
    --- call that with params + saved autoApprove state as is_auto_approved_in_server field
    if type(auto_approve) == "function" then
        local ok, res = pcall(auto_approve, parsed_params)
        if not ok or type(res) == "string" then
            --- If auto_approve function throws an error, or returns a string, treat it as an error
            status = { approve = false, error = res }
        elseif type(res) == "boolean" then
            --- If auto_approve function returns a boolean, use that as the decision
            status = { approve = res, error = nil }
        end
    elseif type(auto_approve) == "boolean" then
        status = { approve = auto_approve, error = nil }
    end

    -- Check if auto-approval is enabled in servers.json
    if parsed_params.is_auto_approved_in_server then
        status = { approve = true, error = nil }
    end

    if status.error then
        return { error = status.error or "Something went wrong with auto-approval", approve = false }
    end

    if status.approve == false and parsed_params.needs_confirmation_window then
        local confirmed, _ = M.show_mcp_tool_prompt(parsed_params)
        return { error = not confirmed and "User cancelled the operation", approve = confirmed }
    end
    return status
end

return M

---@brief [[
--- Logs view for MCPHub UI
--- Shows server output and errors
---@brief ]]
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local View = require("mcphub.ui.views.base")
local renderer = require("mcphub.utils.renderer")

---@class LogsView:View
---@field active_tab "logs"|"issues" Currently active tab
local LogsView = setmetatable({}, {
    __index = View,
})
LogsView.__index = LogsView

function LogsView:new(ui)
    ---@class View
    local instance = View:new(ui, "logs") -- Create base view with name
    instance.active_tab = "logs"
    return setmetatable(instance, LogsView)
end

function LogsView:render_tabs()
    local tabs = {
        {
            text = "Server Logs",
            selected = self.active_tab == "logs",
        },
        {
            text = "Issues",
            selected = self.active_tab == "issues",
        },
    }
    return Text.create_tab_bar(tabs, self:get_width())
end

function LogsView:before_enter()
    View.before_enter(self)

    -- Set up keymaps
    self.keymaps = {
        ["<Tab>"] = {
            action = function()
                self.active_tab = self.active_tab == "logs" and "issues" or "logs"
                self:draw()
            end,
            desc = "Next tab",
        },
        ["<S-Tab>"] = {
            action = function()
                -- Since we only have 2 tabs, reverse is the same as forward
                self.active_tab = self.active_tab == "logs" and "issues" or "logs"
                self:draw()
            end,
            desc = "Previous tab",
        },
        ["x"] = {
            action = function()
                if self.active_tab == "logs" then
                    State.server_output.entries = {}
                else
                    State:clear_errors()
                end
                self:draw()
            end,
            desc = "Clear current tab",
        },
    }
end

function LogsView:render()
    -- Get base header
    local lines = self:render_header(false)

    -- Add tab bar
    table.insert(lines, self:render_tabs())
    -- table.insert(lines, self:divider())
    table.insert(lines, Text.empty_line())

    -- Show empty state placeholders when no content
    if self.active_tab == "logs" and #State.server_output.entries == 0 then
        table.insert(lines, Text.pad_line("No server logs yet", Text.highlights.muted))
    elseif self.active_tab == "issues" and #State.errors.items == 0 then
        table.insert(lines, Text.pad_line("No issues found - All systems running smoothly", Text.highlights.muted))
    else
        -- Render content based on active tab
        if self.active_tab == "logs" then
            -- Show server output entries
            vim.list_extend(lines, renderer.render_server_entries(State.server_output.entries))
        else
            -- Show all errors with details
            vim.list_extend(lines, renderer.render_hub_errors(nil, true))
        end
    end

    return lines
end

return LogsView

---@brief [[
--- Help view for MCPHub UI
--- Shows plugin documentation and keybindings
---@brief ]]
local Text = require("mcphub.utils.text")
local View = require("mcphub.ui.views.base")

---@class HelpView:View
---@field active_tab string Currently active tab
local HelpView = setmetatable({}, {
    __index = View,
})
HelpView.__index = HelpView

function HelpView:new(ui)
    ---@class View
    local instance = View:new(ui, "help")
    instance.tabs = {
        { id = "welcome", text = "Welcome" },
        { id = "troubleshooting", text = "Troubleshooting" },
        { id = "native", text = "Native Servers" },
        { id = "changelog", text = "Changelog" },
    }
    instance.active_tab = instance.tabs[1].id
    instance = setmetatable(instance, HelpView)
    return instance
end

function HelpView:cycle_tab()
    for i, tab in ipairs(self.tabs) do
        if tab.id == self.active_tab then
            -- Move to next tab or wrap around to first
            self.active_tab = (self.tabs[i + 1] or self.tabs[1]).id
            break
        end
    end
end

function HelpView:cycle_tab_reverse()
    for i, tab in ipairs(self.tabs) do
        if tab.id == self.active_tab then
            -- Move to previous tab or wrap around to last
            self.active_tab = (self.tabs[i - 1] or self.tabs[#self.tabs]).id
            break
        end
    end
end

function HelpView:render_tabs()
    local tabs = vim.tbl_map(function(tab)
        return {
            text = tab.text,
            selected = self.active_tab == tab.id,
        }
    end, self.tabs)
    return Text.create_tab_bar(tabs, self:get_width())
end

function HelpView:get_initial_cursor_position()
    -- Position after header
    local lines = self:render_header()
    return #lines + 2
end

function HelpView:before_enter()
    View.before_enter(self)

    -- Set up keymaps
    self.keymaps = {
        ["<Tab>"] = {
            action = function()
                self:cycle_tab()
                self:draw()
            end,
            desc = "Next tab",
        },
        ["<S-Tab>"] = {
            action = function()
                self:cycle_tab_reverse()
                self:draw()
            end,
            desc = "Previous tab",
        },
    }
end

function HelpView:render()
    -- Get base header
    local lines = self:render_header(false)

    -- Add tab bar
    table.insert(lines, self:render_tabs())
    table.insert(lines, Text.empty_line())

    -- Get prompt utils for accessing documentation
    local prompt_utils = require("mcphub.utils.prompt")

    -- Render content based on active tab
    if self.active_tab == "welcome" then
        -- Welcome message
        local welcome_content = [[
# Welcome to MCPHub!

MCP Hub is a MCP client for neovim that seamlessly integrates [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) servers into your editing workflow. It provides an intuitive interface for managing, testing, and using MCP servers with your favorite chat plugins.

> Visit [Documentation](https://ravitemer.github.io/mcphub.nvim/) site for detailed instructions on how to use MCPHub.

## Support Development

MCPHub is an open-source project that relies on community support. Your support helps maintain and enhance the plugin.

- [GitHub Sponsors](https://github.com/sponsors/ravitemer)
- [Buy me a coffee](https://www.buymeacoffee.com/ravitemer)
- [⭐ Star us on GitHub](https://github.com/ravitemer/mcphub.nvim)

### Get Help & Contribute
- [View Documentation](https://ravitemer.github.io/mcphub.nvim/) - Learn more about MCPHub
- [Discord Community](https://discord.gg/NTqfxXsNuN) - Get help and discuss features
- [Open a Discussion](https://github.com/ravitemer/mcphub.nvim/discussions) - Ask questions and share ideas
- [Create an Issue](https://github.com/ravitemer/mcphub.nvim/issues) - Report bugs
- [Report Security Issues](https://github.com/ravitemer/mcphub.nvim/blob/main/SECURITY.md)

### Share with the Community
- Create your own Native MCP Servers and share them in [Show and Tell](https://github.com/ravitemer/mcphub.nvim/discussions/categories/show-and-tell)
- Share your custom workflows and setups in [Native Servers](https://github.com/ravitemer/mcphub.nvim/discussions/categories/native-servers)
- Help others by sharing your configuration tips and tricks
- Showcase your innovative uses of MCPHub

### Join & Connect
- [Discord Community](https://discord.gg/NTqfxXsNuN) - Get help and discuss features
- [Follow on Twitter](https://x.com/ravitemer) - Stay updated on new releases
- Star the repository to show your support!

### Stay Updated
- Check the **Changelog** tab for latest features
- Watch **Discussions** for announcements and community showcases
- Browse **Marketplace** for new MCP servers
]]
        vim.list_extend(lines, Text.render_markdown(welcome_content))
    elseif self.active_tab == "troubleshooting" then
        local troubleshooting_content = prompt_utils.get_troubleshooting_guide()
        vim.list_extend(lines, Text.render_markdown(troubleshooting_content))
    elseif self.active_tab == "native" then
        -- Native server documentation
        local native_guide = prompt_utils.get_native_server_prompt()
        if native_guide then
            vim.list_extend(lines, Text.render_markdown(native_guide))
        else
            table.insert(lines, Text.pad_line("Native server guide not found", Text.highlights.error))
        end
    else -- changelog
        local changelog = prompt_utils.get_plugin_changelog()
        if changelog then
            vim.list_extend(lines, Text.render_markdown(changelog))
        else
            table.insert(lines, Text.pad_line("Changelog not found", Text.highlights.error))
        end
    end

    return lines
end

return HelpView

local Base = require("mcphub.ui.capabilities.base")
local NuiLine = require("mcphub.utils.nuiline")
local State = require("mcphub.state")
local Text = require("mcphub.utils.text")
local highlights = require("mcphub.utils.highlights").groups

---@class PromptsHandler : CapabilityHandler
local PromptsHandler = setmetatable({}, {
    __index = Base,
})
PromptsHandler.__index = PromptsHandler
PromptsHandler.type = "prompts"

function PromptsHandler:new(server_name, capability_info, view)
    local handler = Base.new(self, server_name, capability_info, view)
    return handler
end

function PromptsHandler:handle_action() end

function PromptsHandler:render(line_offset)
    line_offset = line_offset or 0
    self:clear_line_tracking()

    local lines = {}
    local hub = State.hub_instance
    if not hub or not hub:is_ready() then
        vim.list_extend(lines, self:render_section_start("System Prompts"))
        vim.list_extend(lines, self:render_section_content({ NuiLine():append("Hub not ready", highlights.error) }, 2))
        vim.list_extend(lines, self:render_section_end())
        return lines
    end

    local prompts = hub:generate_prompts()
    if not prompts then
        vim.list_extend(lines, self:render_section_start("System Prompts"))
        vim.list_extend(
            lines,
            self:render_section_content({ NuiLine():append("No prompts available", highlights.muted) }, 2)
        )
        vim.list_extend(lines, self:render_section_end())
        return lines
    end

    -- Active Servers Section
    if prompts.active_servers then
        table.insert(lines, Text.pad_line(NuiLine():append("Active Servers Prompt", highlights.title)))
        table.insert(lines, Text.empty_line())
        vim.list_extend(lines, vim.tbl_map(Text.pad_line, Text.render_markdown(prompts.active_servers)))
        -- vim.list_extend(lines, self:render_section_end())
    end

    -- -- Tool Usage Section
    -- if prompts.use_mcp_tool then
    --     vim.list_extend(lines, self:render_section_start("Tool Usage Prompt", highlights.title))
    --     vim.list_extend(lines, self:render_section_content(Text.multiline(prompts.use_mcp_tool), 2))
    --     vim.list_extend(lines, self:render_section_end())
    --     table.insert(lines, Text.empty_line())
    -- end

    -- -- Resource Access Section
    -- if prompts.access_mcp_resource then
    --     vim.list_extend(lines, self:render_section_start("Resource Access Prompt", highlights.title))
    --     vim.list_extend(lines, self:render_section_content(Text.multiline(prompts.access_mcp_resource), 2))
    --     vim.list_extend(lines, self:render_section_end())
    -- end

    return lines
end

return PromptsHandler

local keymap_utils = require("mcphub.native.neovim.utils.keymap")
local text = require("mcphub.utils.text")

---@class EditUI
---@field config UIConfig Configuration options
---@field state UIState Current UI state
---@field highlights table Highlight management
local EditUI = {}
EditUI.__index = EditUI

-- UI State tracking
---@class UIState
---@field origin_winnr integer Window number of the start
---@field bufnr integer Buffer number being edited
---@field file_path string Path to the file
---@field located_blocks LocatedBlock[] Blocks to process
---@field original_content string Original file content
---@field callbacks table Success/cancel callbacks
---@field augroup integer Autocommand group ID
---@field is_replacing_entire_file boolean Whether the entire file is being replaced
---@field hunk_extmarks table<string, integer[]> A map of block_id to a list of extmark IDs for its hunks
---@field has_completed boolean Whether the session has been completed
---@field hunk_blocks DiffHunk[] Generated hunk blocks for granular navigation
---@field completed_hunks table<string, string> Map of hunk_id to completion status
---@field current_hunk_index integer Currently active hunk (1-based)
---@field original_keymaps table<string, table|nil> Stored original keymaps before session

-- Default UI configuration
local DEFAULT_CONFIG = {
    auto_navigate = true,
    go_to_origin_on_complete = true,
    send_diagnostics = true,
    wait_for_diagnostics = 1000,
    diagnostic_severity = vim.diagnostic.severity.WARN, -- Only show warnings and above by default
    keybindings = {
        accept = ".", -- Accept current change
        reject = ",", -- Reject current change
        next = "n", -- Next diff
        prev = "p", -- Previous diff
        accept_all = "ga", -- Accept all remaining changes
        reject_all = "gr", -- Reject all remaining changes
    },
}

-- Create new EditUI instance
---@param config UIConfig? Optional configuration
---@return EditUI
function EditUI.new(config)
    local self = setmetatable({}, EditUI)
    self.config = vim.tbl_deep_extend("force", DEFAULT_CONFIG, config or {})
    self.state = nil
    self.highlights = {
        namespace_diff = vim.api.nvim_create_namespace("mcphub-editor-diff"),
        namespace_hints = vim.api.nvim_create_namespace("mcphub-editor-hints"),
        priority = 20000,
    }
    return self
end

-- Start interactive editing session
---@param opts table Session options
function EditUI:start_interactive_editing(opts)
    local buf_utils = require("mcphub.native.neovim.utils.buffer")
    local bufnr = buf_utils.open_file_in_editor(opts.file_path)
    if not bufnr then
        return opts.on_error("Couldn't open buffer for editing")
    end
    self.state = {
        origin_winnr = opts.origin_winnr,
        bufnr = bufnr,
        is_replacing_entire_file = opts.is_replacing_entire_file,
        file_path = opts.file_path,
        located_blocks = opts.located_blocks,
        original_content = opts.original_content,
        hunk_extmarks = {},
        current_hunk_index = 1,
        completed_hunks = {},
        callbacks = {
            on_complete = opts.on_complete,
            on_cancel = opts.on_cancel,
        },
        has_completed = false,
        augroup = vim.api.nvim_create_augroup("mcphub_editor_ui", { clear = true }),
    }

    self:_setup_autocmds()
    self:_apply_all_changes()

    -- Generate hunk blocks for granular navigation
    self:_generate_hunk_blocks()

    -- Early completion if no actual changes were found
    if not self.state.hunk_blocks or #self.state.hunk_blocks == 0 then
        return self:_complete_session()
    end

    if opts.interactive == false then
        return self:_handle_save()
    end

    self:_highlight_all_blocks()

    self:_setup_keybindings()

    if self.config.auto_navigate then
        self:_navigate_to_hunk(1)
    end

    self:_update_hints()
end
-- Apply all changes from located blocks to the buffer
function EditUI:_apply_all_changes()
    local base_line_offset = 0

    local sorted_blocks = self.state.located_blocks
    table.sort(sorted_blocks, function(a, b)
        return a.location_result.start_line < b.location_result.start_line
    end)

    -- Apply each block's changes
    for _, block in ipairs(sorted_blocks) do
        local start_line = block.location_result.start_line + base_line_offset
        local end_line = block.location_result.end_line + base_line_offset

        -- Apply the replacement
        vim.api.nvim_buf_set_lines(
            self.state.bufnr,
            start_line - 1, -- Convert to 0-based
            end_line,
            false,
            block.replace_lines
        )

        -- Update the block's position after changes
        block.applied_start_line = start_line
        block.applied_end_line = start_line + #block.replace_lines - 1

        -- Calculate offset for next blocks
        local line_diff = #block.replace_lines - (end_line - start_line + 1)
        base_line_offset = base_line_offset + line_diff
    end
end

---Generate hunk blocks from located blocks for granular navigation
---@return DiffHunk[]
function EditUI:_generate_hunk_blocks()
    self.state.hunk_blocks = {}

    for _, located_block in ipairs(self.state.located_blocks) do
        -- Calculate precise diff hunks
        local precise_diff_hunks = {}
        local location_result = located_block.location_result
        if location_result.found then
            precise_diff_hunks = vim.diff(
                table.concat(location_result.found_lines, "\n"),
                table.concat(located_block.replace_lines, "\n"),
                { result_type = "indices", algorithm = "histogram", ctxlen = 3 }
            ) or {} --[[@as table<integer, integer>]]
        end
        -- Process each precise_diff_hunk
        for hunk_index, hunk in ipairs(precise_diff_hunks) do
            local old_start_idx, old_count, new_start_idx, new_count = unpack(hunk)

            -- Extract actual old/new content for this hunk
            local old_lines = {}
            for i = old_start_idx, old_start_idx + old_count - 1 do
                table.insert(old_lines, located_block.location_result.found_lines[i] or "")
            end

            local new_lines = {}
            for i = new_start_idx, new_start_idx + new_count - 1 do
                table.insert(new_lines, located_block.replace_lines[i] or "")
            end

            -- Determine hunk type
            local hunk_type = "change"
            if old_count == 0 then
                hunk_type = "addition"
            elseif new_count == 0 then
                hunk_type = "deletion"
            end

            -- Calculate absolute position in buffer
            local absolute_start = located_block.applied_start_line + new_start_idx - 1
            local absolute_end = absolute_start + new_count - 1

            -- For pure deletions, position is where content was removed
            if new_count == 0 then
                absolute_start = located_block.applied_start_line + old_start_idx - 1
                absolute_end = absolute_start -- No actual lines in buffer
            end

            ---@type DiffHunk
            local hunk_block = {
                hunk_id = string.format("%s_hunk_%d", located_block.block_id, hunk_index),
                parent_id = located_block.block_id,
                old_lines = old_lines,
                new_lines = new_lines,
                type = hunk_type,
                applied_start_line = absolute_start,
                applied_end_line = absolute_end,
                extmark_id = nil, -- Will be set during highlighting
                deletion_position = new_count == 0 and absolute_start or nil,
                confidence = located_block.location_result.confidence or 100, -- Default to 100% confidence
            }

            table.insert(self.state.hunk_blocks, hunk_block)
        end
    end
    return self.state.hunk_blocks
end

-- Highlight hunk blocks in the buffer (new hunk-based approach)
function EditUI:_highlight_all_blocks()
    vim.api.nvim_buf_clear_namespace(self.state.bufnr, self.highlights.namespace_diff, 0, -1)

    if not self.state.hunk_blocks then
        return
    end

    for _, hunk_block in ipairs(self.state.hunk_blocks) do
        self:_highlight_hunk_block(hunk_block)
    end
end

--- Highlight a single hunk block with proper boundary handling
---@param hunk_block DiffHunk Block to highlight
function EditUI:_highlight_hunk_block(hunk_block)
    -- Handle new content highlighting (changes and additions)
    if hunk_block.type == "change" or hunk_block.type == "addition" then
        hunk_block.extmark_id = vim.api.nvim_buf_set_extmark(
            self.state.bufnr,
            self.highlights.namespace_diff,
            hunk_block.applied_start_line - 1,
            0,
            {
                hl_group = text.highlights.diff_add,
                end_row = hunk_block.applied_end_line,
                hl_eol = true,
                hl_mode = "combine",
                priority = self.highlights.priority,
            }
        )
    end

    -- Handle old content as virtual lines (changes and deletions)
    if hunk_block.type == "change" or hunk_block.type == "deletion" then
        local virt_lines = {}
        local function pad_line(line)
            local max_cols = vim.o.columns
            local line_length = #line
            if line_length < max_cols then
                return line .. string.rep(" ", max_cols - line_length)
            end
            return line
        end

        -- Add deletion indicator for pure deletions
        if hunk_block.type == "deletion" then
            table.insert(virt_lines, {
                {
                    pad_line(
                        string.format(
                            "[DELETED %d line%s]",
                            #hunk_block.old_lines,
                            #hunk_block.old_lines > 1 and "s" or ""
                        )
                    ),
                    text.highlights.diff_delete,
                },
            })
        end

        -- Add old content lines
        for _, line in ipairs(hunk_block.old_lines) do
            table.insert(virt_lines, { { pad_line(line), text.highlights.diff_delete } })
        end

        -- Calculate virtual line placement with boundary handling
        local virt_line_row, virt_lines_above = self:_calculate_virtual_line_placement(hunk_block)

        local extmark_opts = {
            virt_lines = virt_lines,
            virt_lines_above = virt_lines_above,
            priority = self.highlights.priority,
        }
        -- For pure deletions, store the extmark_id for navigation
        if hunk_block.type == "deletion" then
            hunk_block.extmark_id = vim.api.nvim_buf_set_extmark(
                self.state.bufnr,
                self.highlights.namespace_diff,
                virt_line_row,
                0,
                extmark_opts
            )
            hunk_block.del_extmark_id = hunk_block.extmark_id
        else
            -- For changes, just add virtual lines (extmark_id already set above)
            hunk_block.del_extmark_id = vim.api.nvim_buf_set_extmark(
                self.state.bufnr,
                self.highlights.namespace_diff,
                virt_line_row,
                0,
                extmark_opts
            )
        end
    end
end

--- Calculate proper placement for virtual lines handling file boundaries
---@param hunk_block  DiffHunk block to calculate placement for
---@return integer virt_line_row The row to place the virtual line
---@return boolean virt_lines_above Whether to place virtual lines above the target line
function EditUI:_calculate_virtual_line_placement(hunk_block)
    local file_line_count = vim.api.nvim_buf_line_count(self.state.bufnr)
    local target_line = hunk_block.applied_start_line - 1 -- Convert to 0-based

    -- At start of file (line 0 or 1)
    if target_line <= 0 then
        if #hunk_block.new_lines > 0 then
            return #hunk_block.new_lines - 1, false -- Place at line 0, above (since below doesn't work at start)
        end
        return 0, false -- Place at line 0, below (since above doesn't work at start)
    end

    -- At end of file
    if target_line >= file_line_count then
        return file_line_count - 1, false -- Place at last line, below
    end

    -- Middle of file - place above the target line
    return target_line, true
end

-- Set up keybindings for diff operations (hunk-based)
function EditUI:_setup_keybindings()
    local bufnr = self.state.bufnr

    local KEYBINDINGS = self.config.keybindings

    -- Store original keymaps before setting temporary ones
    self.state.original_keymaps = keymap_utils.store_original_keymaps("n", KEYBINDINGS, bufnr)

    -- Accept current change
    vim.keymap.set({ "n" }, KEYBINDINGS.accept, function()
        self:_accept_current_hunk()
    end, { buffer = bufnr, desc = "Accept current hunk" })

    -- Reject current change
    vim.keymap.set({ "n" }, KEYBINDINGS.reject, function()
        self:_reject_current_hunk()
    end, { buffer = bufnr, desc = "Reject current hunk" })

    -- Navigate to next hunk
    vim.keymap.set({ "n" }, KEYBINDINGS.next, function()
        self:_navigate_next_hunk()
    end, { buffer = bufnr, desc = "Go to next hunk" })

    -- Navigate to previous hunk
    vim.keymap.set({ "n" }, KEYBINDINGS.prev, function()
        self:_navigate_prev_hunk()
    end, { buffer = bufnr, desc = "Go to previous hunk" })

    -- Accept all remaining changes
    vim.keymap.set({ "n" }, KEYBINDINGS.accept_all, function()
        self:_accept_all_remaining_hunks()
    end, { buffer = bufnr, desc = "Accept all remaining hunks" })

    -- Reject all remaining changes
    vim.keymap.set({ "n" }, KEYBINDINGS.reject_all, function()
        self:_reject_all_remaining_hunks()
    end, { buffer = bufnr, desc = "Reject all remaining hunks" })
end

-- Set up autocommands for buffer events
function EditUI:_setup_autocmds()
    local bufnr = self.state.bufnr

    -- Update hints on cursor movement
    vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
        buffer = bufnr,
        group = self.state.augroup,
        callback = vim.schedule_wrap(function()
            self:_update_hints()
        end),
    })

    -- Handle buffer save
    vim.api.nvim_create_autocmd({ "BufWritePost" }, {
        buffer = bufnr,
        group = self.state.augroup,
        callback = vim.schedule_wrap(function()
            self:_handle_save()
        end),
    })

    -- Handle buffer close
    vim.api.nvim_create_autocmd({ "BufUnload", "BufWipeout" }, {
        buffer = bufnr,
        group = self.state.augroup,
        callback = vim.schedule_wrap(function()
            self:_handle_buffer_close()
        end),
    })
end

-- Accept all remaining hunks
function EditUI:_accept_all_remaining_hunks()
    if not self.state.hunk_blocks then
        return self:_complete_session()
    end

    for _, hunk in ipairs(self.state.hunk_blocks) do
        if not self.state.completed_hunks[hunk.hunk_id] then
            self:_accept_current_hunk(hunk)
        end
    end
    self:_complete_session()
end

-- Reject all remaining hunks
function EditUI:_reject_all_remaining_hunks()
    if not self.state.hunk_blocks then
        return self:_complete_session()
    end

    for _, hunk in ipairs(self.state.hunk_blocks) do
        if not (self.state.completed_hunks and self.state.completed_hunks[hunk.hunk_id]) then
            self:_reject_current_hunk(hunk)
        end
    end
    self:_complete_session()
end

function EditUI:write_if_modified()
    local file = self.state.file_path
    local buf_state = vim.bo[self.state.bufnr]
    if file and (buf_state and buf_state.modified) then
        local ok, err = pcall(vim.cmd.write, file)
        if not ok and err:match("E212") then
            local dir = vim.fn.fnamemodify(file, ":h")
            if vim.fn.isdirectory(dir) == 0 then
                vim.fn.mkdir(dir, "p")
            end
            vim.cmd.write(file)
        end
    end
end

-- Navigate to next hunk
---@return boolean success True if navigated to next hunk
function EditUI:_navigate_next_hunk()
    if not self.state.hunk_blocks then
        return false
    end

    local current_index = self.state.current_hunk_index or 1

    -- Find next uncompleted hunk
    for i = current_index + 1, #self.state.hunk_blocks do
        local hunk = self.state.hunk_blocks[i]
        if not (self.state.completed_hunks and self.state.completed_hunks[hunk.hunk_id]) then
            if self:_navigate_to_hunk(i) then
                self.state.current_hunk_index = i
                return true
            end
        end
    end

    -- Wrap to beginning
    for i = 1, current_index do
        local hunk = self.state.hunk_blocks[i]
        if not (self.state.completed_hunks and self.state.completed_hunks[hunk.hunk_id]) then
            if self:_navigate_to_hunk(i) then
                self.state.current_hunk_index = i
                return true
            end
        end
    end

    return false -- No more hunks
end

-- Navigate to previous hunk
---@return boolean success True if navigated to previous hunk
function EditUI:_navigate_prev_hunk()
    if not self.state.hunk_blocks then
        return false
    end

    local current_index = self.state.current_hunk_index or 1

    -- Find previous uncompleted hunk
    for i = current_index - 1, 1, -1 do
        local hunk = self.state.hunk_blocks[i]
        if not (self.state.completed_hunks and self.state.completed_hunks[hunk.hunk_id]) then
            if self:_navigate_to_hunk(i) then
                self.state.current_hunk_index = i
                return true
            end
        end
    end

    -- Wrap to end
    for i = #self.state.hunk_blocks, current_index + 1, -1 do
        local hunk = self.state.hunk_blocks[i]
        if not (self.state.completed_hunks and self.state.completed_hunks[hunk.hunk_id]) then
            if self:_navigate_to_hunk(i) then
                self.state.current_hunk_index = i
                return true
            end
        end
    end

    return false -- No more hunks
end

-- Validate hunk block and return location info to avoid duplicate extmark fetching
---@param hunk_block table Hunk block to validate
---@return table result Validation result with location info
function EditUI:_validate_hunk_block(hunk_block)
    -- Check if extmark_id exists
    if not hunk_block.extmark_id then
        return {
            status = "no_extmark_id",
            navigable = false,
            start_row = hunk_block.deletion_position or hunk_block.applied_start_line or 0,
            end_row = hunk_block.deletion_position or hunk_block.applied_end_line or 0,
        }
    end

    local extmark_details = vim.api.nvim_buf_get_extmark_by_id(
        self.state.bufnr,
        self.highlights.namespace_diff,
        hunk_block.extmark_id,
        { details = true }
    )

    -- Return both validation and location info
    local start_row = extmark_details[1] or 0
    local end_row = extmark_details[3] and extmark_details[3].end_row or start_row

    local result = {
        start_row = start_row,
        end_row = end_row,
        extmark_details = extmark_details,
    }

    -- Check if extmark exists
    if #extmark_details == 0 then
        result.status = "extmark_missing"
        result.navigable = false
        return result
    end

    -- Special handling for pure deletions
    if hunk_block.type == "deletion" then
        -- For deletions, extmark should point to virtual line with deletion marker
        result.status = "deletion_marker_present"
        result.navigable = true
        result.end_row = start_row -- Same line for deletion marker
        return result
    end

    -- Check if hunk was deleted (start == end but we expect multiple lines)
    if start_row == end_row and #hunk_block.new_lines > 1 then
        result.status = "content_deleted"
        result.navigable = false
        return result
    end

    -- Get current content in the range for additions and changes
    local current_lines = vim.api.nvim_buf_get_lines(self.state.bufnr, start_row, end_row, false)

    -- Check content status
    if vim.deep_equal(current_lines, hunk_block.new_lines) then
        result.status = "matches_new"
        result.navigable = true
    elseif vim.deep_equal(current_lines, hunk_block.old_lines) then
        result.status = "matches_old"
        result.navigable = true
    else
        result.status = "user_modified"
        result.navigable = true
    end
    return result
end

-- Navigate to specific hunk using validation result (avoids duplicate extmark fetching)
---@param hunk_index integer Hunk index to navigate to
---@return boolean success True if navigation was successful
function EditUI:_navigate_to_hunk(hunk_index)
    if not self.state.hunk_blocks then
        return false
    end

    local hunk_block = self.state.hunk_blocks[hunk_index]
    if not hunk_block then
        return false
    end

    local validation = self:_validate_hunk_block(hunk_block)
    if not validation.navigable then
        -- Skip this hunk, try next
        return self:_find_next_navigable_hunk(hunk_index)
    end

    -- Use validation.start_row directly, no need to fetch extmark again
    local winid = self:_get_window_for_buffer()
    if winid then
        vim.api.nvim_win_set_cursor(winid, { validation.start_row + 1, 0 })
        vim.api.nvim_win_call(winid, function()
            vim.cmd("normal! zz")
        end)
        return true
    end

    return false
end

-- Reject current hunk using validation result (avoids duplicate extmark fetching)
---@param hunk_block table? Optional specific hunk to reject
function EditUI:_reject_current_hunk(hunk_block)
    local target_hunk = hunk_block or self:_get_current_hunk()
    if not target_hunk then
        return
    end

    local validation = self:_validate_hunk_block(target_hunk)
    if not validation.navigable then
        -- Hunk is not available, just mark as completed
        self.state.completed_hunks[target_hunk.hunk_id] = "skipped"
        return
    end

    -- Handle different hunk types for rejection
    if target_hunk.type == "deletion" then
        -- For deletions, we need to restore the deleted content
        -- Insert the old lines at the deletion position
        local insert_pos = target_hunk.deletion_position or validation.start_row
        vim.api.nvim_buf_set_lines(
            self.state.bufnr,
            insert_pos - 1,
            insert_pos - 1, -- Insert without replacing
            false,
            target_hunk.old_lines
        )
    elseif target_hunk.type == "addition" then
        -- For additions, remove the added lines
        vim.api.nvim_buf_set_lines(
            self.state.bufnr,
            validation.start_row,
            validation.end_row,
            false,
            {} -- Remove lines
        )
    else
        -- For changes, replace new content with old content
        vim.api.nvim_buf_set_lines(
            self.state.bufnr,
            validation.start_row,
            validation.end_row,
            false,
            target_hunk.old_lines
        )
    end

    -- Mark as rejected and remove highlights
    self.state.completed_hunks = self.state.completed_hunks or {}
    self.state.completed_hunks[target_hunk.hunk_id] = "rejected"
    self:_remove_hunk_highlights(target_hunk)
    if not self:_navigate_next_hunk() then
        return self:_complete_session()
    end
end

-- Accept current hunk (just remove highlights and mark as accepted)
---@param hunk_block table? Optional specific hunk to accept
function EditUI:_accept_current_hunk(hunk_block)
    local target_hunk = hunk_block or self:_get_current_hunk()
    if not target_hunk then
        return
    end

    -- Mark as accepted and remove highlights
    self.state.completed_hunks = self.state.completed_hunks or {}
    self.state.completed_hunks[target_hunk.hunk_id] = "accepted"
    self:_remove_hunk_highlights(target_hunk)
    if not self:_navigate_next_hunk() then
        return self:_complete_session()
    end
end

-- Find next navigable hunk starting from given index
---@param current_index integer Current hunk index
---@return boolean success True if found and navigated to next hunk
function EditUI:_find_next_navigable_hunk(current_index)
    if not self.state.hunk_blocks then
        return false
    end

    -- Search forward from current position
    for i = current_index + 1, #self.state.hunk_blocks do
        local hunk_block = self.state.hunk_blocks[i]
        if not self.state.completed_hunks[hunk_block.hunk_id] then
            local validation = self:_validate_hunk_block(hunk_block)
            if validation.navigable then
                return self:_navigate_to_hunk(i)
            end
        end
    end

    -- Wrap around to beginning
    for i = 1, current_index do
        local hunk_block = self.state.hunk_blocks[i]
        if not self.state.completed_hunks[hunk_block.hunk_id] then
            local validation = self:_validate_hunk_block(hunk_block)
            if validation.navigable then
                return self:_navigate_to_hunk(i)
            end
        end
    end

    return false -- No more navigable hunks
end

-- Get current hunk based on cursor position
---@return DiffHunk? Current hunk block or nil if not found
function EditUI:_get_current_hunk()
    if not self.state.hunk_blocks then
        return
    end

    local winid = self:_get_window_for_buffer()
    if not winid then
        return
    end

    local cursor_line = vim.api.nvim_win_get_cursor(winid)[1] - 1

    for _, hunk_block in ipairs(self.state.hunk_blocks) do
        if self.state.completed_hunks[hunk_block.hunk_id] then
            goto continue
        end

        local validation = self:_validate_hunk_block(hunk_block)
        if validation.navigable then
            if cursor_line >= validation.start_row and cursor_line <= validation.end_row then
                return hunk_block
            end
        end

        ::continue::
    end
end

-- Remove highlights for a specific hunk
---@param hunk_block DiffHunk Hunk block to remove highlights from
function EditUI:_remove_hunk_highlights(hunk_block)
    if hunk_block.extmark_id then
        pcall(vim.api.nvim_buf_del_extmark, self.state.bufnr, self.highlights.namespace_diff, hunk_block.extmark_id)
        hunk_block.extmark_id = nil
    end
    if hunk_block.del_extmark_id then
        pcall(vim.api.nvim_buf_del_extmark, self.state.bufnr, self.highlights.namespace_diff, hunk_block.del_extmark_id)
        hunk_block.extmark_id = nil
    end
    -- Clear current hint for the removed block's line
    vim.api.nvim_buf_clear_namespace(self.state.bufnr, self.highlights.namespace_hints, 0, -1)
end

-- Get window ID for the buffer
---@return integer? winid Window ID or nil if not found
function EditUI:_get_window_for_buffer()
    for _, winid in ipairs(vim.api.nvim_list_wins()) do
        if vim.api.nvim_win_get_buf(winid) == self.state.bufnr then
            return winid
        end
    end
    return nil
end

-- Restore original content for a block
---@param block LocatedBlock Block to restore
function EditUI:_restore_block_original_content(block)
    local start_line = block.applied_start_line or 1
    local end_line = block.applied_end_line or 1
    vim.api.nvim_buf_set_lines(self.state.bufnr, start_line - 1, end_line, false, block.location_result.found_lines)

    -- Update the block's position after restoration
    block.applied_end_line = start_line + #block.location_result.found_lines - 1
end

-- Update hint display (hunk-based)
function EditUI:_update_hints()
    if not self.state or not self.state.hunk_blocks then
        return
    end

    vim.api.nvim_buf_clear_namespace(self.state.bufnr, self.highlights.namespace_hints, 0, -1)
    local current_hunk = self:_get_current_hunk()
    if not current_hunk then
        return
    end

    -- For all hunks of a block, show the block confidence
    local confidence = current_hunk.confidence or 0
    local current_index = self.state.current_hunk_index or 1
    local index = string.format("%d/%d", current_index, #self.state.hunk_blocks)

    -- Get position from hunk validation for accurate placement
    local validation = self:_validate_hunk_block(current_hunk)
    local start_line = math.max(0, validation.start_row)

    -- If at start of buffer, place hint at a visible location
    if start_line == 0 then
        start_line = math.max(1, validation.end_row or 1)
    end
    local file_line_count = vim.api.nvim_buf_line_count(self.state.bufnr)
    -- -- At end of file
    if start_line >= file_line_count - 1 then
        start_line = file_line_count
    end

    vim.api.nvim_buf_set_extmark(self.state.bufnr, self.highlights.namespace_hints, start_line - 1, 0, {
        virt_text = self:_create_hint_line(confidence, index),
        virt_text_pos = "right_align",
        priority = self.highlights.priority,
    })
end

-- Handle buffer save
function EditUI:_handle_save()
    -- If there are still pending hunks, treat save as accept all
    local has_pending = #vim.tbl_filter(function(hunk)
        return not self.state.completed_hunks[hunk.hunk_id]
    end, self.state.hunk_blocks) > 0

    if has_pending then
        return self:_accept_all_remaining_hunks()
    end
    self:_complete_session()
end

-- Handle buffer close
function EditUI:_handle_buffer_close()
    if not self.state or self.state.has_completed then
        return
    end
    self.state.callbacks.on_cancel("User closed the editor without accepting the changes.")
end

-- Complete the editing session
function EditUI:_complete_session()
    if not self.state or self.state.has_completed then
        return
    end
    self.state.has_completed = true
    self:write_if_modified()
    vim.schedule(function()
        local origin_winnr = self.state.origin_winnr
        -- Signal completion to the session without arguments
        self.state.callbacks.on_complete()
        if self.config.go_to_origin_on_complete and origin_winnr and vim.api.nvim_win_is_valid(origin_winnr) then
            pcall(vim.api.nvim_set_current_win, origin_winnr)
        end
    end)
end

--- Generate block-focused summary for LLM
--- @param final_content string Final content after edits
--- @param config table Configuration options for summary generation
--- @return string Summary of block results formatted for markdown
function EditUI:_generate_block_summary(final_content, config)
    -- Handle case where no hunk blocks were generated (no actual changes)
    if not self.state.hunk_blocks or #self.state.hunk_blocks == 0 then
        return string.format(
            "No changes were applied in `%s` file. Because, REPLACE content of all provided SEARCH/REPLACE block(s) was found identical to the content found at that location",
            self.state.file_path
        )
    end

    -- Analyze block results
    local block_results = self:_analyze_block_results()

    -- Count block statuses
    local fully_applied = 0
    local partially_applied = 0
    local fully_rejected = 0

    for _, result in pairs(block_results) do
        if result.status == "FULLY_APPLIED" then
            fully_applied = fully_applied + 1
        elseif result.status == "PARTIALLY_APPLIED" then
            partially_applied = partially_applied + 1
        else
            fully_rejected = fully_rejected + 1
        end
    end

    local total_blocks = fully_applied + partially_applied + fully_rejected
    local summary = ""

    -- Simple messages for straightforward cases
    if fully_applied == total_blocks then
        summary = string.format("All %d block(s) were successfully applied to `%s`", total_blocks, self.state.file_path)
    elseif fully_rejected == total_blocks then
        summary = string.format(
            "All %d block(s) were rejected - no changes were applied to `%s`",
            total_blocks,
            self.state.file_path
        )
    else
        -- Detailed breakdown for mixed results
        local applied_count = fully_applied + partially_applied
        summary = string.format(
            "%d of %d block(s) were applied to `%s`\n\n",
            applied_count,
            total_blocks,
            self.state.file_path
        )

        -- Add details for each block in order
        local sorted_blocks = {}
        for block_id, result in pairs(block_results) do
            table.insert(sorted_blocks, result)
        end
        table.sort(sorted_blocks, function(a, b)
            return a.block_id < b.block_id
        end)

        for _, result in ipairs(sorted_blocks) do
            summary = summary .. self:_format_block_result(result) .. "\n\n"
        end
    end

    -- Always add final diff
    if config.include_final_diff then
        summary = summary .. self:_create_final_diff_section(final_content)
    end

    return summary
end

--- Analyze hunk results grouped by parent blocks
---@return table<integer, table> block_results
function EditUI:_analyze_block_results()
    local block_results = {}

    -- Group hunks by parent block
    for _, hunk in ipairs(self.state.hunk_blocks) do
        local parent_id = hunk.parent_id
        if not block_results[parent_id] then
            block_results[parent_id] = {
                block_id = parent_id,
                rejected_lines = {},
                total_hunks = 0,
                accepted_hunks = 0,
            }
        end

        block_results[parent_id].total_hunks = block_results[parent_id].total_hunks + 1

        local status = self.state.completed_hunks[hunk.hunk_id] or "accepted"
        if status == "accepted" then
            block_results[parent_id].accepted_hunks = block_results[parent_id].accepted_hunks + 1
        else
            block_results[parent_id].rejected_lines = vim.deepcopy(hunk.new_lines)
        end
    end

    -- Determine status for each block
    for _, result in pairs(block_results) do
        if result.accepted_hunks == result.total_hunks then
            result.status = "FULLY_APPLIED"
        elseif result.accepted_hunks == 0 then
            result.status = "REJECTED"
        else
            result.status = "PARTIALLY_APPLIED"
        end
    end

    return block_results
end

--- Format individual block result for summary
--- @param result table Block result containing status and details
--- @return string Formatted block result for markdown
function EditUI:_format_block_result(result)
    if result.status == "FULLY_APPLIED" then
        return string.format("### %s: FULLY APPLIED\nAll changes from this block were accepted.", result.block_id)
    elseif result.status == "REJECTED" then
        return string.format("### %s: REJECTED\nAll changes from this block were rejected.", result.block_id)
    else
        -- PARTIALLY_APPLIED
        local rejected_content = table.concat(result.rejected_lines, "\n")
        return string.format(
            "### %s: PARTIALLY APPLIED\nThe following lines from your REPLACE content were NOT accepted by the user:\n```\n%s\n```",
            result.block_id,
            rejected_content
        )
    end
end

---Create final diff section only for edits
---@param final_content string Final content after edits
---@param original_content string? Original content (fallback to state if not provided)
---@return string Final diff section formatted for markdown
function EditUI:_create_final_diff_section(final_content, original_content)
    local function add_new_lines(content)
        if content:sub(-1) ~= "\n" then
            return content .. "\n"
        end
        return content
    end

    -- Use provided original content or fallback to state
    local orig_content = original_content or self.state.original_content

    -- Avoiding diff generation if the entire file is being replaced
    if self.state.is_replacing_entire_file then
        return ""
    end

    local diff_output = vim.trim(
        vim.diff(add_new_lines(orig_content), add_new_lines(final_content), { result_type = "unified", ctxlen = 1 })
            or ""
    )

    return string.format(
        "\n\n### BEFORE vs AFTER EDIT SESSION DIFF for `%s`:\nIMPORTANT: Carefully observe this diff to understand the changes applied in the edit session including user edits or formatters etc \n```diff\n%s\n```",
        self.state.file_path,
        diff_output
    )
end

--- Add diagnostic feedback to the summary
--- @param summary string Initial summary text
--- @param config table Configuration options for diagnostics
--- @param callback function Callback to execute with the final summary
function EditUI:_add_diagnostic_feedback(summary, config, callback)
    local lsp_feedback = ""
    local bufnr = self.state.bufnr
    local file_path = self.state.file_path
    if not config.send_diagnostics then
        return callback(summary)
    end
    vim.defer_fn(function()
        local min_severity = config.diagnostic_severity or vim.diagnostic.severity.WARN
        local all_diagnostics = vim.diagnostic.get(bufnr)
        local diagnostics = vim.tbl_filter(function(diag)
            return diag.severity <= min_severity
        end, all_diagnostics)
        if #diagnostics > 0 then
            lsp_feedback = string.format("\n\n## CURRENT DIAGNOSTICS FOR `%s` :\n", file_path)

            -- Get file content for context
            local file_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)

            -- Group diagnostics by line number
            local diag_by_line = {}
            for _, diag in ipairs(diagnostics) do
                local line_num = diag.lnum + 1 -- Convert to 1-based
                if not diag_by_line[line_num] then
                    diag_by_line[line_num] = {}
                end
                table.insert(diag_by_line[line_num], diag)
            end

            -- Format each line with its diagnostics
            for line_num in vim.spairs(diag_by_line) do
                local line_content = file_lines[line_num] or ""
                local number = string.format("%d ", line_num)
                lsp_feedback = lsp_feedback .. string.format("\n%s| %s\n", number, line_content)

                for _, diag in ipairs(diag_by_line[line_num]) do
                    lsp_feedback = lsp_feedback
                        .. string.format(
                            string.rep(" ", #number) .. "| %s (%s from %s)\n",
                            diag.message or "No message",
                            vim.diagnostic.severity[diag.severity],
                            diag.source or "Unknown"
                        )
                end
            end
        end
        callback(summary .. lsp_feedback)
    end, config.wait_for_diagnostics or 0)
end

-- Generate comprehensive summary of the editing session
---@param config table Options including final_content, original_content, config
---@param callback function Callback to receive the summary string
function EditUI:get_summary(config, callback)
    if not self.state then
        return callback("")
    end

    local file_lines = vim.api.nvim_buf_get_lines(self.state.bufnr, 0, -1, false)
    local final_content = table.concat(file_lines, "\n")

    local summary_parts = {}

    -- Generate session summary if enabled
    if config.include_session_summary then
        local session_summary = self:_generate_block_summary(final_content, config)
        if session_summary and session_summary ~= "" then
            table.insert(summary_parts, session_summary)
        end
    end

    local base_summary = table.concat(summary_parts, "\n\n")

    -- Add diagnostic feedback if enabled (asynchronous)
    if config.send_diagnostics then
        self:_add_diagnostic_feedback(base_summary, config, callback)
    else
        callback(base_summary)
    end
end

-- Cleanup UI resources
function EditUI:cleanup()
    if not self.state then
        return
    end

    local bufnr = self.state.bufnr

    -- Clear namespaces
    if vim.api.nvim_buf_is_valid(bufnr) then
        vim.api.nvim_buf_clear_namespace(bufnr, self.highlights.namespace_diff, 0, -1)
        vim.api.nvim_buf_clear_namespace(bufnr, self.highlights.namespace_hints, 0, -1)

        -- Restore original keymaps and clean up temporary ones
        if self.state.original_keymaps then
            keymap_utils.restore_keymaps("n", self.config.keybindings, bufnr, self.state.original_keymaps)
        end
    end

    -- Clean up autocommands
    pcall(vim.api.nvim_del_augroup_by_id, self.state.augroup)

    self.state = nil
end

--- Create a hint line for the current hunk
--- @param confidence number Confidence percentage of the current hunk
--- @param progress string Progress string in the format "current/total"
--- @return table Hint line as a table of text segments with highlights
function EditUI:_create_hint_line(confidence, progress)
    local KEYBINDINGS = self.config.keybindings
    local hint_line = {}
    local hl_type = confidence < 100 and "warn" or "success"
    table.insert(hint_line, { "", text.highlights[hl_type] })
    table.insert(hint_line, {
        string.format(" %d%% │ %s ", confidence, progress),
        text.highlights[hl_type .. "_fill"],
    })

    local full_line = string.format(
        " accept (%s) reject (%s) accept-all (%s) reject-all (%s) ",
        KEYBINDINGS.accept,
        KEYBINDINGS.reject,
        KEYBINDINGS.accept_all,
        KEYBINDINGS.reject_all
    )
    table.insert(hint_line, { full_line, "ColorColumn" })
    table.insert(hint_line, { "", text.highlights[hl_type] })
    return hint_line
end

return EditUI

local Error = require("mcphub.utils.errors")
local Job = require("plenary.job")
local State = require("mcphub.state")
local config = require("mcphub.config")
local config_manager = require("mcphub.utils.config_manager")
local constants = require("mcphub.utils.constants")
local curl = require("plenary.curl")
local version = require("mcphub.utils.version")
local workspace_utils = require("mcphub.utils.workspace")

local handlers = require("mcphub.utils.handlers")
local log = require("mcphub.utils.log")
local native = require("mcphub.native")
local prompt_utils = require("mcphub.utils.prompt")
local utils = require("mcphub.utils")

-- Default timeouts
local CONNECT_TIMEOUT = 1000 -- 1s for curl to connect to localhost
local TOOL_TIMEOUT = 60000 -- 60s for tool calls
local RESOURCE_TIMEOUT = 60000 -- 60s for resource access
local PROMPT_TIMEOUT = 60000 -- 60s for tool calls
local MCP_REQUEST_TIMEOUT = 60000 -- 60s for MCP requests

--- @class MCPHub.Hub
--- @field port number The port number for the MCP Hub server
--- @field server_url string In case of hosting mcp-hub somewhere, the url with `https://mydomain.com:5858`
--- @field config string Path to the MCP servers configuration file
--- @field auto_toggle_mcp_servers boolean whether to enable LLM to start and stop MCP Servers
--- @field shutdown_delay number Delay in seconds before shutting down the server
--- @field cmd string The cmd to invoke the MCP Hub server
--- @field cmdArgs table The args to pass to the cmd to spawn the server
--- @field ready boolean Whether the connection to server is ready
--- @field server_job Job|nil The server process job if we started it
--- @field is_owner boolean Whether this instance started the server
--- @field is_shutting_down boolean Whether we're in the process of shutting down
--- @field mcp_request_timeout number --Max time allowed for a MCP tool or resource to execute in milliseconds, set longer for long running tasks
--- @field on_ready fun(hub)
--- @field on_error fun(error:string)
--- @field setup_opts table Original options used to create this hub instance
local MCPHub = {}
MCPHub.__index = MCPHub

--- Create a new MCPHub instance
--- @param opts table Configuration options
--- @return MCPHub.Hub
function MCPHub:new(opts)
    local instance = setmetatable({
        port = opts.port,
        server_url = opts.server_url,
        config = opts.config,
        auto_toggle_mcp_servers = opts.auto_toggle_mcp_servers,
        shutdown_delay = opts.shutdown_delay,
        cmd = opts.cmd,
        cmdArgs = opts.cmdArgs,
        ready = false,
        server_job = nil,
        is_owner = false,
        is_shutting_down = false,
        is_starting = false,
        mcp_request_timeout = opts.mcp_request_timeout or MCP_REQUEST_TIMEOUT,
        on_ready = opts.on_ready or function() end,
        on_error = opts.on_error or function() end,
        setup_opts = opts,
    }, MCPHub)

    -- Setup global state tracking
    instance:_setup_global_state_tracking()

    return instance
end

--- Resolve context (workspace vs global) for the current directory
--- @return MCPHub.JobContext|nil Context information or nil on error
function MCPHub:resolve_context()
    if not State.config.workspace.enabled then
        return self:_resolve_global_context()
    end

    return self:_resolve_workspace_context()
end

--- Resolve workspace-specific context
--- @return MCPHub.JobContext|nil Workspace context or nil to fall back to global
function MCPHub:_resolve_workspace_context()
    local cwd = vim.fn.getcwd()

    -- Find workspace config
    local workspace_info = workspace_utils.find_workspace_config(State.config.workspace.look_for, cwd)

    if not workspace_info then
        -- No workspace config found, fall back to global mode
        log.debug("No workspace config found, falling back to global mode")
        return self:_resolve_global_context()
    end

    log.debug("Found workspace config" .. vim.inspect(workspace_info))

    local port

    -- Prepare config files (order matters: global first, then project)
    local global_config = self.config -- Original global config path
    local project_config = workspace_info.config_file
    local config_files = { global_config, project_config }

    -- Check for existing hub using root and config_file because using hashed port might not be reliable when we found next available port if a port is occupied
    local existing_hub = workspace_utils.find_matching_workspace_hub(workspace_info.root_dir, config_files)
    if existing_hub then
        port = existing_hub.port
        log.debug("Found existing workspace hub" .. vim.inspect({
            workspace_root = workspace_info.root_dir,
            port = port,
            pid = existing_hub.pid,
        }))
    else
        if State.config.workspace.get_port and type(State.config.workspace.get_port) == "function" then
            -- Use custom port function if provided
            port = State.config.workspace.get_port()
            if not port or type(port) ~= "number" then
                vim.notify("Invalid port returned by workspace.get_port function", vim.log.levels.ERROR)
                return nil
            end
            log.debug("Using custom port from workspace.get_port: " .. port)
        else
            -- Generate new port
            port = workspace_utils.find_available_port(
                workspace_info.root_dir,
                State.config.workspace.port_range,
                100 -- max attempts
            )
        end

        if not port then
            vim.notify("No available ports for workspace hub", vim.log.levels.ERROR)
            return nil
        end

        log.debug("Generated new port for workspace" .. vim.inspect({
            workspace_root = workspace_info.root_dir,
            port = port,
        }))
    end

    return {
        port = port,
        cwd = workspace_info.root_dir or cwd,
        config_files = config_files,
        is_workspace_mode = true,
        workspace_root = workspace_info.root_dir or cwd,
        existing_hub = existing_hub, -- Include hub info from cache
    }
end

--- Resolve global context (original behavior)
--- @return MCPHub.JobContext Global context
function MCPHub:_resolve_global_context()
    local cwd = vim.fn.getcwd()
    local port = self.setup_opts.port
    -- For global mode, port will always be the one provided in setup_opts, so we can get the existing hub using that
    local existing_hub = workspace_utils.get_workspace_hub_info(port)
    return {
        port = port,
        cwd = cwd,
        config_files = { self.config },
        is_workspace_mode = false,
        workspace_root = cwd,
        existing_hub = existing_hub,
    }
end

--- Resolve global environment variables
--- @param context MCPHub.JobContext Hub context (workspace info, port, etc.)
--- @return table Resolved global environment variables
function MCPHub:_resolve_global_env(context)
    local global_env_config = config.global_env
    local resolved_global_env = {}

    -- Handle function type
    if type(global_env_config) == "function" then
        local success, result = pcall(global_env_config, context)
        if not success then
            vim.notify("global_env function failed: " .. result, vim.log.levels.WARN)
            return {}
        end
        if type(result) ~= "table" then
            vim.notify("global_env function must return a table", vim.log.levels.WARN)
            return {}
        end
        global_env_config = result
    elseif type(global_env_config) ~= "table" then
        if global_env_config ~= nil then
            vim.notify("global_env must be table or function", vim.log.levels.WARN)
        end
        return {}
    end

    -- Process mixed array/hash format
    for key, value in pairs(global_env_config) do
        if type(key) == "number" then
            -- Array-style entry: just the env var name
            if type(value) == "string" then
                local env_value = os.getenv(value)
                if env_value then
                    resolved_global_env[value] = env_value
                end
            end
        else
            -- Hash-style entry: key = value
            if type(value) == "string" then
                resolved_global_env[key] = value
            end
        end
    end

    return resolved_global_env
end

--- Start server with resolved context
--- @param context MCPHub.JobContext Context information from resolve_context
function MCPHub:_start_server_with_context(context)
    self.is_owner = true
    if self.server_job then
        self.server_job = nil
    end

    -- Resolve global environment variables
    local resolved_global_env = self:_resolve_global_env(context)

    -- Build command args with config files
    local args = utils.clean_args({
        self.cmdArgs,
        "--port",
        tostring(context.port),
    })

    -- Add all config files (global first, then project-specific)
    for _, config_file in ipairs(context.config_files) do
        table.insert(args, "--config")
        table.insert(args, config_file)
    end

    -- Add other flags
    vim.list_extend(args, {
        "--auto-shutdown",
        "--shutdown-delay",
        self.shutdown_delay or 0,
        "--watch",
    })

    -- Prepare job environment with global env
    local job_env = {}
    if next(resolved_global_env) then
        -- Serialize global env for mcp-hub
        local success, json_env = pcall(vim.fn.json_encode, resolved_global_env)
        if success then
            job_env.MCP_HUB_ENV = json_env
            log.debug("Passing global environment variables to mcp-hub: " .. vim.inspect({
                count = vim.tbl_count(resolved_global_env),
                keys = vim.tbl_keys(resolved_global_env),
            }))
        else
            vim.notify("Failed to serialize global_env: " .. json_env, vim.log.levels.WARN)
        end
    end
    job_env = vim.tbl_extend("force", vim.fn.environ(), { CWD = context.cwd }, job_env or {})

    log.debug("Starting server with context" .. vim.inspect({
        port = context.port,
        cwd = context.cwd,
        config_files = context.config_files,
        is_workspace_mode = context.is_workspace_mode,
    }))

    ---@diagnostic disable-next-line: missing-fields
    self.server_job = Job:new({
        command = self.cmd,
        args = args,
        cwd = context.cwd, -- Set working directory
        env = job_env, -- Pass environment variables
        hide = true,
        on_stderr = vim.schedule_wrap(function(_, data)
            if data then
                log.debug("Server stderr:" .. data)
            end
        end),
        on_start = vim.schedule_wrap(function()
            self:connect_sse()
        end),
        on_stdout = vim.schedule_wrap(function(_, _)
            -- if data then
            --     log.debug("Server stdout:" .. data)
            -- end
        end),
        on_exit = vim.schedule_wrap(function(j, code)
            local stderr = table.concat(j:stderr_result() or {}, "\n")
            log.debug("Server process exited with code " .. code .. " and stderr: " .. stderr)
            if stderr:match("EADDRINUSE") then
                -- The on_start's self:connect_sse() will handle this case
                log.debug("Port taken, trying to connect...")
            else
                -- This is causing issues when switching workspaces frequently even when we check for server_job
                -- sse_job's on_exit will anyway takes care of showing stopped status in case the mcp-hub was stopped externally
                -- local err_msg = "Server process exited with code " .. code
                -- self:handle_hub_stopped(err_msg .. "\n" .. stderr, code, j)
            end
        end),
    })

    self.server_job:start()
end

--- Start the MCP Hub server
function MCPHub:start()
    if self.is_starting then
        vim.notify("MCP Hub is starting", vim.log.levels.WARN)
        return
    end
    log.debug("Starting hub")
    State:clear_logs()
    -- Update state
    State:update_hub_state(constants.HubState.STARTING)
    self.is_restarting = false
    self.is_starting = true

    -- Resolve context (workspace vs global)
    local context = self:resolve_context()
    if not context then
        self:handle_hub_stopped("Failed to resolve hub context")
        return
    end

    -- Update state with resolved context
    State.current_hub = context
    self.port = context.port -- Update our port for this session

    -- Load config cache into state
    local cache_loaded, refresh_err = config_manager.refresh_config()
    if not cache_loaded then
        self:handle_hub_stopped("Failed to load MCP servers configuration:" .. (refresh_err or ""))
        return
    end
    log.debug("Resolved hub context: " .. vim.inspect(context))

    -- Make sure to load after the config is refreshed to avoid double reading of config files
    native.setup(config.native_servers)

    -- Step 3: Check if server is already running on the resolved port and is of same version in cases of plugin updated
    self:check_server(function(is_running, is_our_server, is_same_version)
        if is_running then
            if not is_our_server then
                self:handle_hub_stopped("Port in use by non-MCP Hub server")
                return
            end
            if not is_same_version then
                log.debug("Existing server is not of same version, restarting")
                vim.notify("mcp-hub version mismatch. Restarting hub...", vim.log.levels.INFO)
                self:handle_same_port_different_config(context)
                return
            end
            if not context.existing_hub then
                log.debug(
                    "Port available but no existing hub info, might be due to changed config files, starting new server"
                )
                self:handle_same_port_different_config(context)
                return
            end

            -- Check config compatibility using cached hub info
            if context.existing_hub and context.existing_hub.config_files then
                local config_matches = vim.deep_equal(context.existing_hub.config_files, context.config_files)
                if config_matches then
                    log.debug("Config files match, connecting to existing server")
                    self:connect_sse()
                else
                    log.debug("Config files changed, restarting server")
                    vim.notify("Config files changed. Restarting hub...", vim.log.levels.INFO)
                    self:handle_same_port_different_config(context)
                end
            else
                -- No config info available (backwards compatibility)
                log.debug("No config info in cache, connecting to existing server")
                self:connect_sse()
            end
            return
        end

        -- Step 4: Start new server with resolved context
        self:_start_server_with_context(context)
    end)
end

--- Handle directory changes - reconnect to appropriate workspace hub
function MCPHub:handle_directory_change()
    if not State.config.workspace.enabled then
        return
    end

    log.debug("Directory changed, checking if workspace hub should change")

    -- Resolve new context for current directory
    local new_context = self:resolve_context()
    if not new_context then
        log.warn("Failed to resolve context after directory change")
        return
    end

    -- Compare with current context
    local current_context = State.current_hub
    if not current_context then
        log.debug("No current hub context, starting new hub")
        self:start()
        return
    end

    -- Check if we need to switch hubs
    local needs_switch = false
    local reason = ""

    if tostring(new_context.port) ~= tostring(current_context.port) then
        needs_switch = true
        reason = string.format("port change (%s -> %s)", current_context.port, new_context.port)
    end

    if needs_switch then
        log.debug("Switching hub due to: " .. reason)
        -- self:restart(nil, reason)
        self:stop_sse()
        vim.schedule(function()
            self:start()
        end)
    else
        log.debug("No hub switch needed - context unchanged")
    end
end

function MCPHub:handle_hub_ready()
    self.ready = true
    self.is_restarting = false
    self.is_starting = false
    self:_update_global_state()
    self.on_ready(self)
    self:update_servers()
    if State.marketplace_state.status == "empty" then
        self:get_marketplace_catalog()
    end
end

function MCPHub:_clean_up()
    self.is_owner = false
    self.ready = false
    self.is_starting = false
    self.is_restarting = false
    self:stop_sse()
    State:update_hub_state(constants.HubState.STOPPED)
    self:_update_global_state()
end

---@param msg string
---@param code number|nil
function MCPHub:handle_hub_stopped(msg, code, server_job)
    if server_job then
        -- While changing directories, we might have to start a new job for that directory
        -- Once in the new directory, if the old directory's job stops for any reason for e.g (shutdown_delay reached)
        -- handle_hub_stopped will be called which causes the hub UI to show as stopped.
        -- Therefore, we need to check if the server_job is the same as the one that called this function
        if self.server_job ~= server_job then
            return
        end
    end
    code = code ~= nil and code or 1
    -- if self.is_shutting_down then
    --     return -- Skip error handling during shutdown
    -- end
    if code ~= 0 then
        -- Create error object
        local err = Error("SERVER", Error.Types.SERVER.SERVER_START, msg)
        State:add_error(err)
        self.on_error(tostring(err))
    end
    self:_clean_up()
end

--- Check if server is running and handle connection
--- @param callback fun(is_running:boolean, is_our_server:boolean, is_same_version:boolean) Callback function to handle the result
function MCPHub:check_server(callback)
    log.debug("Checking Server")
    if self:is_ready() then
        return callback(true, true, true)
    end
    -- Quick health check
    local opts = {
        timeout = 3000,
        skip_ready_check = true,
    }

    opts.callback = function(response, err)
        if err then
            log.debug("Error while get health in check_server")
            callback(false, false, false)
        else
            local is_hub_server = response and response.server_id == "mcp-hub" and response.status == "ok"
            local is_same_version = response and response.version == version.REQUIRED_NODE_VERSION.string
            log.debug(
                string.format(
                    "Got health response in check_server, is_hub_server (%s), is_same_version (%s) ",
                    tostring(is_hub_server),
                    tostring(is_same_version)
                )
            )
            callback(true, is_hub_server, is_same_version) -- Running but may not be our server
        end
    end
    return self:get_health(opts)
end

--- Get server status information
--- @param opts? { callback?: fun(response: table?, error: string?) } Optional callback(response: table|nil, error?: string)
--- @return table|nil, string|nil If no callback is provided, returns response and error
function MCPHub:get_health(opts)
    return self:api_request("GET", "health", opts)
end

---@param name string
---@return NativeServer | MCPServer | nil
function MCPHub:get_server(name)
    local is_native = native.is_native_server(name)
    if is_native then
        return is_native
    end
    for _, server in ipairs(State.server_state.servers) do
        if server.name == name then
            return server
        end
    end
    return nil
end

---@param url string The OAuth callback URL
function MCPHub:handle_oauth_callback(url, callback)
    if not url or vim.trim(url) == "" then
        return vim.notify("No OAuth callback URL provided", vim.log.levels.ERROR)
    end
    self:api_request("POST", "oauth/manual_callback", {
        body = {
            url = url,
        },
        callback = callback,
    })
end

---@param name any
---@param _ any
function MCPHub:authorize_mcp_server(name, _)
    -- local authUrl = self:get_server(name)["authorizationUrl"]
    self:api_request("POST", "servers/authorize", {
        body = {
            server_name = name,
        },
        callback = function(response, _)
            --Errors will be handled automatically
            if response and response.authorizationUrl then
                vim.notify("Opening Authorization URL: " .. response.authorizationUrl, vim.log.levels.INFO)
            else
                vim.notify("No Authorization URL found for server: " .. name, vim.log.levels.WARN)
            end
        end,
    })
end

--- Start a disabled/disconnected MCP server
---@param name string Server name to start
---@param opts? { via_curl_request?:boolean,callback?: function }
---@return table?, string? If no callback is provided, returns response and error
function MCPHub:start_mcp_server(name, opts)
    opts = opts or {}
    if not self:update_server_config(name, {
        disabled = false,
    }) then
        return
    end
    local is_native = native.is_native_server(name)
    if is_native then
        local server = is_native
        server:start()
        State:emit("servers_updated", {
            hub = self,
        })
    else
        for i, server in ipairs(State.server_state.servers) do
            if server.name == name then
                State.server_state.servers[i].status = "connecting"
                break
            end
        end

        --only if we want to send a curl request (otherwise file watch and sse events autoupdates)
        --This is needed in cases where users need to start the server and need to be sure if it is started or not rather than depending on just file watching
        --Note: this will update the config in the state in the backend which will not trigger file change event as this is sometimes updated before the file change event is triggered so the backend explicitly sends SubscriptionEvent with type servers_updated. which leads to "no signigicant changes" notification as well as "servers updated" notification as we send this explicitly.
        if opts.via_curl_request then
            -- Call start endpoint
            self:api_request("POST", "servers/start", {
                body = {
                    server_name = name,
                },
                callback = function(response, err)
                    self:refresh()
                    if opts.callback then
                        opts.callback(response, err)
                    end
                end,
            })
        end
    end
    State:notify_subscribers({
        server_state = true,
    }, "server")
end

--- Stop an MCP server
---@param name string Server name to stop
---@param disable boolean Whether to disable the server
---@param opts? { via_curl_request?: boolean, callback?: function } Optional callback(response: table|nil, error?: string)
---@return table|nil, string|nil If no callback is provided, returns response and error
function MCPHub:stop_mcp_server(name, disable, opts)
    opts = opts or {}

    if not self:update_server_config(name, {
        disabled = disable or false,
    }) then
        return
    end
    local is_native = native.is_native_server(name)
    if is_native then
        local server = is_native
        server:stop()
        State:emit("servers_updated", {
            hub = self,
        })
    else
        for i, server in ipairs(State.server_state.servers) do
            if server.name == name then
                State.server_state.servers[i].status = "disconnecting"
                break
            end
        end

        --only if we want to send a curl request (otherwise file watch and sse events autoupdates)
        if opts.via_curl_request then
            -- Call stop endpoint
            self:api_request("POST", "servers/stop", {
                query = disable and {
                    disable = "true",
                } or nil,
                body = {
                    server_name = name,
                },
                callback = function(response, err)
                    self:refresh()
                    if opts.callback then
                        opts.callback(response, err)
                    end
                end,
            })
        end
    end
    State:notify_subscribers({
        server_state = true,
    }, "server")
end

--- Get a prompt from the server
--- @param server_name string
--- @param prompt_name string
--- @param args table
--- @param opts? {parse_response?: boolean, callback?: fun(res: MCPResponseOutput? ,err: string?), request_options?: MCPRequestOptions, timeout?: number } Optional callback(response: table|nil, error?: string) and timeout in ms (default 60s)
--- @return {messages : {role:"user"| "assistant"|"system", output: MCPResponseOutput}[]}|nil, string|nil If no callback is provided, returns response and error
function MCPHub:get_prompt(server_name, prompt_name, args, opts)
    opts = opts or {}
    if opts.callback then
        local original_callback = opts.callback
        opts.callback = function(response, err)
            -- Signal prompt completion
            utils.fire("MCPHubPromptEnd", {
                server = server_name,
                prompt = prompt_name,
                success = err == nil,
            })
            if opts.parse_response == true then
                response = prompt_utils.parse_prompt_response(response)
            end
            if original_callback then
                original_callback(response, err)
            end
        end
    end

    local request_options =
        vim.tbl_deep_extend("force", { timeout = self.mcp_request_timeout }, opts.request_options or {})
    -- Signal prompt start
    utils.fire("MCPHubPromptStart", {
        server = server_name,
        prompt = prompt_name,
    })
    local arguments = args or vim.empty_dict()
    if vim.islist(arguments) or vim.isarray(arguments) then
        if #arguments == 0 then
            arguments = vim.empty_dict()
        else
            log.error("Arguments should be a dictionary, but got a list.")
            return
        end
    end
    --make sure we have an object
    -- Check native servers first
    local is_native = native.is_native_server(server_name)
    if is_native then
        local server = is_native
        local result, err = server:get_prompt(prompt_name, args, opts)
        if opts.callback == nil then
            utils.fire("MCPHubPromptEnd", {
                server = server_name,
                prompt = prompt_name,
                success = err == nil,
            })
            return (opts.parse_response == true and prompt_utils.parse_prompt_response(result) or result), err
        end
        return
    end

    local response, err = self:api_request(
        "POST",
        "servers/prompts",
        vim.tbl_extend("force", {
            timeout = (request_options.timeout + 5000) or PROMPT_TIMEOUT,
            body = {
                server_name = server_name,
                prompt = prompt_name,
                arguments = arguments,
                request_options = request_options,
            },
        }, opts)
    )

    -- handle sync calls
    if opts.callback == nil then
        utils.fire("MCPHubPromptEnd", {
            server = server_name,
            prompt = prompt_name,
            success = err == nil,
        })
        return (opts.parse_response == true and prompt_utils.parse_prompt_response(response) or response), err
    end
end

--- Call a tool on a server
--- @param server_name string
--- @param tool_name string
--- @param args table
--- @param opts? {parse_response?: boolean, callback?: fun(res: MCPResponseOutput? ,err: string?), request_options?: MCPRequestOptions, timeout?: number } Optional callback(response: table|nil, error?: string) and timeout in ms (default 60s)
--- @return MCPResponseOutput?, string? If no callback is provided, returns response and error
function MCPHub:call_tool(server_name, tool_name, args, opts)
    opts = opts or {}
    if opts.callback then
        local original_callback = opts.callback
        opts.callback = function(response, err)
            -- Signal tool completion
            utils.fire("MCPHubToolEnd", {
                server = server_name,
                tool = tool_name,
                response = prompt_utils.parse_tool_response(response),
                success = err == nil,
            })
            if opts.parse_response == true then
                response = prompt_utils.parse_tool_response(response)
            end
            if original_callback then
                original_callback(response, err)
            end
        end
    end
    local request_options =
        vim.tbl_deep_extend("force", { timeout = self.mcp_request_timeout }, opts.request_options or {})
    -- Signal tool start
    utils.fire("MCPHubToolStart", {
        server = server_name,
        tool = tool_name,
    })
    local arguments = args or vim.empty_dict()
    if vim.islist(arguments) or vim.isarray(arguments) then
        if #arguments == 0 then
            arguments = vim.empty_dict()
        else
            log.error("Arguments should be a dictionary, but got a list.")
            return
        end
    end
    -- Check native servers first
    local is_native = native.is_native_server(server_name)
    if is_native then
        local server = is_native
        local result, err = server:call_tool(tool_name, args, opts)
        if opts.callback == nil then
            utils.fire("MCPHubToolEnd", {
                server = server_name,
                tool = tool_name,
                response = prompt_utils.parse_tool_response(result),
                success = err == nil,
            })
            return (opts.parse_response == true and prompt_utils.parse_tool_response(result) or result), err
        end
        return
    end

    local response, err = self:api_request(
        "POST",
        "servers/tools",
        vim.tbl_extend("force", {
            ---Make sure that actual curl request timeout is more than the MCP request timeout
            timeout = (request_options.timeout + 5000) or TOOL_TIMEOUT,
            body = {
                server_name = server_name,
                tool = tool_name,
                arguments = arguments,
                request_options = request_options,
            },
        }, opts)
    )

    -- handle sync calls
    if opts.callback == nil then
        utils.fire("MCPHubToolEnd", {
            server = server_name,
            tool = tool_name,
            response = prompt_utils.parse_tool_response(response),
            success = err == nil,
        })
        return (opts.parse_response == true and prompt_utils.parse_tool_response(response) or response), err
    end
end

--- Access a server resource
--- @param server_name string
--- @param uri string
--- @param opts? {parse_response?: boolean, callback?: fun(res: MCPResponseOutput? ,err: string?), request_options?: MCPRequestOptions, timeout?: number } Optional callback(response: table|nil, error?: string) and timeout in ms (default 60s)
--- @return MCPResponseOutput?, string? If no callback is provided, returns response and error
function MCPHub:access_resource(server_name, uri, opts)
    opts = opts or {}
    if opts.callback then
        local original_callback = opts.callback
        opts.callback = function(response, err)
            -- Signal resource completion
            utils.fire("MCPHubResourceEnd", {
                server = server_name,
                uri = uri,
                success = err == nil,
            })
            if opts.parse_response == true then
                response = prompt_utils.parse_resource_response(response)
            end
            if original_callback then
                original_callback(response, err)
            end
        end
    end

    local request_options =
        vim.tbl_deep_extend("force", { timeout = self.mcp_request_timeout }, opts.request_options or {})
    -- Signal resource start
    utils.fire("MCPHubResourceStart", {
        server = server_name,
        uri = uri,
    })

    -- Check native servers first
    local is_native = native.is_native_server(server_name)
    if is_native then
        local server = is_native
        local result, err = server:access_resource(uri, opts)
        if opts.callback == nil then
            utils.fire("MCPHubResourceEnd", {
                server = server_name,
                uri = uri,
                success = err == nil,
            })
            return (opts.parse_response == true and prompt_utils.parse_resource_response(result) or result), err
        end
        return
    end

    -- Otherwise proxy to MCP server
    local response, err = self:api_request(
        "POST",
        "servers/resources",
        vim.tbl_extend("force", {
            ---Make sure that actual curl request timeout is more than the MCP request timeout
            timeout = (request_options.timeout + 5000) or RESOURCE_TIMEOUT,
            body = {
                server_name = server_name,
                uri = uri,
                request_options = request_options,
            },
        }, opts)
    )
    -- handle sync calls
    if opts.callback == nil then
        utils.fire("MCPHubResourceEnd", {
            server = server_name,
            uri = uri,
            success = err == nil,
        })
        return (opts.parse_response == true and prompt_utils.parse_resource_response(response) or response), err
    end
end

--- API request helper
--- @param method string HTTP method
--- @param path string API path
--- @param opts? { body?: table, timeout?: number, skip_ready_check?: boolean, callback?: fun(response: table?, error: string?), query?: table }
--- @return table|nil, string|nil If no callback is provided, returns response and error
function MCPHub:api_request(method, path, opts)
    opts = opts or {}
    local callback = opts.callback
    -- the url of the mcp-hub server if it is hosted somewhere (e.g. https://mydomain.com)
    local base_url = self.server_url or string.format("http://localhost:%d", self.port)
    --remove any trailing slashes
    base_url = base_url:gsub("/+$", "")

    -- Build URL with query parameters if any
    local url = string.format("%s/api/%s", base_url, path)
    if opts.query then
        local params = {}
        for k, v in pairs(opts.query) do
            table.insert(params, k .. "=" .. v)
        end
        url = url .. "?" .. table.concat(params, "&")
    end

    local raw = {}
    vim.list_extend(raw, {
        "--connect-timeout",
        tostring(vim.fn.floor(CONNECT_TIMEOUT / 1000)),
    })
    if opts.timeout then
        local timeout_seconds = tostring(vim.fn.floor((opts.timeout or TOOL_TIMEOUT) / 1000))
        vim.list_extend(raw, {
            "--max-time",
            timeout_seconds,
        })
    end

    -- Prepare request options
    local request_opts = {
        url = url,
        method = method,
        --INFO: generating custom headers file path to avoid getting .header file not found when simulataneous requests are sent
        dump = utils.gen_dump_path(),
        headers = {
            ["Content-Type"] = "application/json",
            ["Accept"] = "application/json",
        },
        raw = raw,
        on_error = vim.schedule_wrap(function(err)
            log.debug(string.format("Error while making request to %s: %s", path, vim.inspect(err)))
            local error = handlers.ResponseHandlers.process_error(err)
            if not self:is_ready() and path == "health" then
                if callback then
                    callback(nil, tostring(error))
                end
            else
                if callback then
                    callback(nil, tostring(error))
                end
                State:add_error(error)
            end
        end),
    }
    if opts.body then
        request_opts.body = vim.fn.json_encode(opts.body)
    end

    -- Only skip ready check for health check
    if not opts.skip_ready_check and not self.ready and path ~= "health" then
        local err = Error("SERVER", Error.Types.SERVER.INVALID_STATE, "MCP Hub not ready")
        State:add_error(err)
        if callback then
            callback(nil, tostring(err))
            return
        else
            return nil, tostring(err)
        end
    end

    -- Process response
    local function process_response(response)
        local curl_error = handlers.ResponseHandlers.handle_curl_error(response, request_opts)
        if curl_error then
            State:add_error(curl_error)
            if callback then
                callback(nil, tostring(curl_error))
                return
            else
                return nil, tostring(curl_error)
            end
        end

        local http_error = handlers.ResponseHandlers.handle_http_error(response, request_opts)
        if http_error then
            State:add_error(http_error)
            if callback then
                callback(nil, tostring(http_error))
                return
            else
                return nil, tostring(http_error)
            end
        end

        local result, parse_error = handlers.ResponseHandlers.parse_json(response.body, request_opts)
        if parse_error then
            State:add_error(parse_error)
            if callback then
                callback(nil, tostring(parse_error))
                return
            else
                return nil, tostring(parse_error)
            end
        end

        if callback then
            callback(result)
        else
            return result
        end
    end

    if callback then
        -- Async mode
        curl.request(vim.tbl_extend("force", request_opts, {
            callback = vim.schedule_wrap(function(response)
                process_response(response)
            end),
        }))
    else
        -- Sync mode
        return process_response(curl.request(request_opts))
    end
end

--- Refresh cache of config files
---@param paths string[]|nil
function MCPHub:reload_config(paths)
    local success, err = config_manager.refresh_config(paths)
    if not success then
        self:handle_hub_stopped("Failed to reload MCP servers configuration: " .. (err or ""))
        return
    end
    -- toggling a native server in one neovim will trigger insignificant changes in other neovim instances, which should be handled by refreshing the native servers
    self:refresh_native_servers()
end

-- make sure we update the native servers disabled status when the servers are updated through a sse event
function MCPHub:refresh_native_servers()
    local updated = false
    for _, server in ipairs(State.server_state.native_servers) do
        local server_config = config_manager.get_server_config(server) or {}
        local is_enabled = server_config.disabled ~= true
        local was_running = server.status == "connected"
        if is_enabled == was_running then
            goto continue
        end
        updated = true
        if not is_enabled then
            server:stop()
        else
            server:start()
        end
        ::continue::
    end
    if updated then
        self:fire_servers_updated()
    end
end

function MCPHub:fire_servers_updated()
    -- Update global state
    self:_update_global_state()

    -- Triggers UI update
    State:notify_subscribers({
        server_state = true,
    }, "server")

    -- Useful for lualine and other extensions
    utils.fire("MCPHubServersUpdated", {
        active_servers = #self:get_servers(),
    })
    State:emit("servers_updated", {
        hub = self,
    })
end

function MCPHub:update_servers(response, callback)
    callback = callback or function() end
    local servers = response and response.servers or nil
    local workspaces = response and response.workspaces or nil
    local function update_state(_servers, workspaces)
        if _servers then
            State.server_state.servers = _servers or {}
        end
        if workspaces then
            State.server_state.workspaces = workspaces or {}
        end
        self:fire_servers_updated()
    end
    if servers or workspaces then
        update_state(servers, workspaces)
    else
        self:get_health({
            callback = function(response, err)
                if err then
                    local health_err = Error("SERVER", Error.Types.SERVER.HEALTH_CHECK, "Health check failed", {
                        error = err,
                    })
                    State:add_error(health_err)
                    callback(false)
                else
                    update_state(response.servers or {}, response.workspaces or {})
                    callback(true)
                end
            end,
        })
    end
end

function MCPHub:handle_capability_updates(data)
    local type = data.type
    local server = data.server
    local map = {
        [constants.SubscriptionTypes.TOOL_LIST_CHANGED] = { "tools" },
        [constants.SubscriptionTypes.RESOURCE_LIST_CHANGED] = { "resources", "resourceTemplates" },
        [constants.SubscriptionTypes.PROMPT_LIST_CHANGED] = { "prompts" },
    }
    local fields_to_update = map[type]
    if not fields_to_update then
        log.warn("Unknown capability update type: " .. type)
        return
    end
    if not server then
        return
    end
    for _, s in ipairs(State.server_state.servers) do
        if s.name == server then
            local emit_data = {
                server = server,
                hub = self,
            }
            for _, field in ipairs(fields_to_update) do
                s.capabilities[field] = data[field] or {}
                emit_data[field] = s.capabilities[field]
            end
            State:emit(type, emit_data)
            break
        end
    end
    -- Notify subscribers of state change
    State:notify_subscribers({
        server_state = true,
    }, "server")
end

--- Update server configuration in the MCP config file
---@param server_name string Name of the server to update
---@param updates table|nil Key-value pairs to update in the server config or nil to remove
---@param opts? { merge:boolean?, config_source: string? } Optional callback(success: boolean)
---@return boolean, string|nil Returns success status and error message if any
function MCPHub:update_server_config(server_name, updates, opts)
    return config_manager.update_server_config(server_name, updates, opts)
end

--- Remove server configuration
---@param server_name string Server ID to remove
---@return boolean, string|nil Returns success status and error message if any
function MCPHub:remove_server_config(server_name)
    -- Use update_server_config with nil updates to remove
    return self:update_server_config(server_name, nil)
end

function MCPHub:stop()
    self.is_shutting_down = true
    -- Stop SSE connection
    self:stop_sse()
    self:_clean_up()
    self.is_shutting_down = false
end

function MCPHub:is_ready()
    return self.ready
end

--- Connect to SSE events endpoint
function MCPHub:connect_sse()
    if self.sse_job then
        return
    end
    local buffer = ""
    local base_url = self.server_url or string.format("http://localhost:%d", self.port)
    base_url = base_url:gsub("/+$", "")

    -- Create SSE connection
    ---@diagnostic disable-next-line: missing-fields
    local sse_job = Job:new({
        command = "curl",
        args = {
            "--no-buffer",
            "--tcp-nodelay",
            "--retry",
            "5",
            "--retry-delay",
            "1",
            "--retry-connrefused",
            "--keepalive-time",
            "60",
            base_url .. "/api/events",
        },
        on_stdout = vim.schedule_wrap(function(_, data)
            if data ~= nil then
                buffer = buffer .. data .. "\n"

                while true do
                    local event_end = buffer:find("\n\n")
                    if not event_end then
                        break
                    end

                    local event_str = buffer:sub(1, event_end - 1)
                    buffer = buffer:sub(event_end + 2)

                    local event = event_str:match("^event: (.-)\n")
                    local data_line = event_str:match("\ndata: ([^\r\n]+)")

                    if event and data_line then
                        local success, decoded = utils.json_decode(data_line)
                        if success then
                            log.trace(string.format("SSE event: %s", event))
                            handlers.SSEHandlers.handle_sse_event(event, decoded, self)
                        else
                            log.warn(string.format("Failed to decode SSE data: %s", data_line))
                        end
                    else
                        log.warn(string.format("Malformed SSE event: %s", event_str))
                    end
                end
            end
        end),
        on_stderr = vim.schedule_wrap(function(_, data)
            log.debug("SSE STDERR: " .. tostring(data))
        end),
        on_exit = vim.schedule_wrap(function(_, code)
            log.debug("SSE JOB exited with " .. tostring(code))
            self.sse_job = nil
            self:handle_hub_stopped("SSE connection failed with code " .. tostring(code), code)
        end),
    })

    -- Store SSE job for cleanup
    self.sse_job = sse_job
    sse_job:start()
end

--- Stop SSE connection
function MCPHub:stop_sse()
    if self.sse_job then
        self.sse_job:shutdown(0)
        self.sse_job = nil
    end
end

function MCPHub:refresh(callback)
    callback = callback or function() end
    self:update_servers(nil, callback)
end

function MCPHub:hard_refresh(callback)
    callback = callback or function() end
    if not self:ensure_ready() then
        return
    end
    self:api_request("GET", "refresh", {
        callback = function(response, err)
            if err then
                local health_err = Error("SERVER", Error.Types.SERVER.HEALTH_CHECK, "Hard Refresh failed : " .. err, {
                    error = err,
                })
                State:add_error(health_err)
                callback(false)
            else
                self:update_servers(response)
                callback(true)
            end
        end,
    })
end

function MCPHub:handle_hub_restarting()
    --for non owner client
    self.is_restarting = true
    State:update({
        errors = {
            items = {},
        },
        server_output = {
            entries = {},
        },
    }, "server")
end

function MCPHub:handle_hub_stopping()
    if self.is_restarting then
        vim.defer_fn(function()
            self:start()
        end, 1000)
    end
end

--- Handles already running mcp-hub on same port with different config files
---@param context table Context containing port and config details
function MCPHub:handle_same_port_different_config(context)
    self:api_request("POST", "hard-restart", {
        callback = function(_, err)
            if err then
                local restart_err = Error("SERVER", Error.Types.SERVER.RESTART, "Hard restart failed", {
                    error = err,
                })
                State:add_error(restart_err)
                vim.notify(
                    "Failed to restart MCP Hub while trying to stop mcp-hub running on port " .. context.port,
                    vim.log.levels.ERROR
                )
                return
            end
            self:_start_server_with_context(context)
        end,
        skip_ready_check = true,
    })
end

--- Restart the MCP Hub server
--- @param callback function|nil Optional callback to execute after restart
--- @param reason string|nil Optional reason for the restart
function MCPHub:restart(callback, reason)
    if not self:ensure_ready() then
        return self:start()
    end
    if self.is_restarting then
        vim.notify("MCP Hub is already restarting.", vim.log.levels.WARN)
        return
    end
    self.is_restarting = true
    self:api_request("POST", "hard-restart", {
        callback = function(_, err)
            if err then
                local restart_err = Error("SERVER", Error.Types.SERVER.RESTART, "Hard restart failed", {
                    error = err,
                })
                State:add_error(restart_err)
                if callback then
                    callback(false)
                end
                return
            end
            if callback then
                callback(true)
            end
        end,
    })
end

function MCPHub:ensure_ready()
    if not self:is_ready() then
        log.warn("Hub is not ready.")
        return false
    end
    return true
end

--- Get servers with their tools filtered based on server config
--- @param server MCPServer The server object to filter
---@return table[] Array of connected servers with disabled tools filtered out
local function filter_server_capabilities(server)
    local config = config_manager.get_server_config(server) or {}
    local filtered_server = vim.deepcopy(server)

    if filtered_server.capabilities then
        -- Common function to filter capabilities
        local function filter_capabilities(capabilities, disabled_list, id_field)
            return vim.tbl_filter(function(item)
                return not vim.tbl_contains(disabled_list, item[id_field])
            end, capabilities)
        end

        -- Filter all capability types with their respective config fields
        local capability_filters = {
            tools = { list = "disabled_tools", id = "name" },
            resources = { list = "disabled_resources", id = "uri" },
            resourceTemplates = { list = "disabled_resourceTemplates", id = "uriTemplate" },
            prompts = { list = "disabled_prompts", id = "name" },
        }

        --- Make the properties field in inputSchema will be encoded as a object not array when empty
        if filtered_server.capabilities.tools then
            for _, tool in ipairs(filtered_server.capabilities.tools) do
                if tool.inputSchema and tool.inputSchema.properties and not next(tool.inputSchema.properties) then
                    tool.inputSchema.properties = vim.empty_dict()
                end
            end
        end

        for cap_type, filter in pairs(capability_filters) do
            if filtered_server.capabilities[cap_type] then
                filtered_server.capabilities[cap_type] =
                    filter_capabilities(filtered_server.capabilities[cap_type], config[filter.list] or {}, filter.id)
            end
        end
    end
    return filtered_server
end

---resolve any functions in the native servers
---@param native_server NativeServer
---@return table
local function resolve_native_server(native_server)
    local server = vim.deepcopy(native_server)
    local possible_func_fields = {
        tools = { "description", "inputSchema" },
        resources = { "description" },
        resourceTemplates = { "description" },
        prompts = { "description" },
    }
    --first resolve the server desc itself
    server.description = prompt_utils.get_description(server)

    for cap_type, fields in pairs(possible_func_fields) do
        for _, capability in ipairs(server.capabilities[cap_type] or {}) do
            --remove handler as it is not in std protocol
            capability.handler = nil
            for _, field in ipairs(fields) do
                if capability[field] then
                    if field == "description" then --resolves to string
                        capability[field] = prompt_utils.get_description(capability)
                    elseif field == "inputSchema" then --resolves to inputSchema table
                        capability[field] = prompt_utils.get_inputSchema(capability)
                    end
                end
            end
        end
    end
    return server
end

---@param include_disabled? boolean
---@return MCPServer[]
function MCPHub:get_servers(include_disabled)
    include_disabled = include_disabled == true
    if not self:is_ready() then
        return {}
    end
    local filtered_servers = {}

    -- Add regular MCP servers
    for _, server in ipairs(State.server_state.servers or {}) do
        if server.status == "connected" or include_disabled then
            local filtered_server = filter_server_capabilities(server)
            table.insert(filtered_servers, filtered_server)
        end
    end

    -- Add native servers
    for _, server in ipairs(State.server_state.native_servers or {}) do
        if server.status == "connected" or include_disabled then
            local filtered_server = filter_server_capabilities(server)
            --INFO: this is for cases where chat plugins expect std MCP definations to remove mcphub specific enhancements
            local resolved_server = resolve_native_server(filtered_server)
            table.insert(filtered_servers, resolved_server)
        end
    end

    return filtered_servers
end

---@return EnhancedMCPPrompt[]
function MCPHub:get_prompts()
    local active_servers = self:get_servers()
    local prompts = {}
    for _, server in ipairs(active_servers) do
        if server.capabilities and server.capabilities.prompts then
            for _, prompt in ipairs(server.capabilities.prompts) do
                table.insert(
                    prompts,
                    vim.tbl_extend("force", prompt, {
                        server_name = server.name,
                    })
                )
            end
        end
    end
    return prompts
end

---@return EnhancedMCPResource[]
function MCPHub:get_resources()
    local active_servers = self:get_servers()
    local resources = {}
    for _, server in ipairs(active_servers) do
        if server.capabilities and server.capabilities.resources then
            for _, resource in ipairs(server.capabilities.resources) do
                table.insert(
                    resources,
                    vim.tbl_extend("force", resource, {
                        server_name = server.name,
                    })
                )
            end
        end
    end
    return resources
end

---@return EnhancedMCPResourceTemplate[]
function MCPHub:get_resource_templates()
    local active_servers = self:get_servers()
    local resource_templates = {}
    for _, server in ipairs(active_servers) do
        if server.capabilities and server.capabilities.resourceTemplates then
            for _, resource_template in ipairs(server.capabilities.resourceTemplates) do
                table.insert(
                    resource_templates,
                    vim.tbl_extend("force", resource_template, {
                        server_name = server.name,
                    })
                )
            end
        end
    end
    return resource_templates
end

---@return EnhancedMCPTool[]
function MCPHub:get_tools()
    local active_servers = self:get_servers()
    local tools = {}
    for _, server in ipairs(active_servers) do
        if server.capabilities and server.capabilities.tools then
            for _, tool in ipairs(server.capabilities.tools) do
                table.insert(
                    tools,
                    vim.tbl_extend("force", tool, {
                        server_name = server.name,
                    })
                )
            end
        end
    end
    return tools
end

--- Convert server to text format
--- @param server MCPServer
--- @return string
function MCPHub:convert_server_to_text(server)
    local filtered_server = filter_server_capabilities(server)
    return prompt_utils.server_to_text(filtered_server)
end

--- Get active servers prompt
--- @param add_example? boolean Whether to add example to the prompt
--- @param include_disabled? boolean Whether to include disabled servers
--- @return string
function MCPHub:get_active_servers_prompt(add_example, include_disabled)
    include_disabled = include_disabled ~= nil and include_disabled or self.auto_toggle_mcp_servers
    add_example = add_example ~= false
    if not self:is_ready() then
        return ""
    end
    return prompt_utils.get_active_servers_prompt(self:get_servers(include_disabled), add_example, include_disabled)
end

--- Get all MCP system prompts
---@param opts? {use_mcp_tool_example?: string, add_example?: boolean, include_disabled?: boolean, access_mcp_resource_example?: string}
---@return {active_servers: string|nil, use_mcp_tool: string|nil, access_mcp_resource: string|nil}
function MCPHub:generate_prompts(opts)
    if not self:ensure_ready() then
        return {}
    end
    opts = opts or {}
    return {
        active_servers = self:get_active_servers_prompt(opts.add_example, opts.include_disabled),
        use_mcp_tool = prompt_utils.get_use_mcp_tool_prompt(opts.use_mcp_tool_example),
        access_mcp_resource = prompt_utils.get_access_mcp_resource_prompt(opts.access_mcp_resource_example),
    }
end

--- Get marketplace catalog with filters
--- @param opts? { search?: string, category?: string, sort?: string, callback?: function, timeout?: number }
--- @return table|nil, string|nil If no callback is provided, returns response and error
function MCPHub:get_marketplace_catalog(opts)
    if State.marketplace_state.status == "loading" then
        return
    end
    opts = opts or {}
    local query = {}

    -- Add filters to query if provided
    if opts.search then
        query.search = opts.search
    end
    if opts.category then
        query.category = opts.category
    end
    if opts.sort then
        query.sort = opts.sort
    end

    State:update({
        marketplace_state = {
            status = "loading",
        },
    }, "marketplace")
    -- Make request with market-specific error handling
    return self:api_request("GET", "marketplace", {
        query = query,
        callback = function(response, err)
            if err then
                local market_err = Error(
                    "MARKETPLACE",
                    Error.Types.MARKETPLACE.FETCH_ERROR,
                    "Failed to fetch marketplace catalog",
                    { error = err }
                )
                State:add_error(market_err)
                State:update({
                    marketplace_state = {
                        status = "error",
                    },
                }, "marketplace")
                return
            end

            -- Update marketplace state
            State:update({
                marketplace_state = {
                    status = "loaded",
                    catalog = {
                        items = response.servers or {},
                        last_updated = response.timestamp,
                    },
                },
            }, "marketplace")
        end,
    })
end

--- Update global variables for lualine integration
function MCPHub:_update_global_state()
    vim.g.mcphub_status = State.server_state.state
    vim.g.mcphub_servers_count = #self:get_servers()
end

--- Setup global variable tracking for tool/resource execution
function MCPHub:_setup_global_state_tracking()
    local group = vim.api.nvim_create_augroup("mcphub_global_state", { clear = true })

    -- Track tool/resource/prompt execution
    vim.api.nvim_create_autocmd("User", {
        group = group,
        pattern = { "MCPHubToolStart", "MCPHubResourceStart", "MCPHubPromptStart" },
        callback = function()
            vim.g.mcphub_executing = true
        end,
    })

    vim.api.nvim_create_autocmd("User", {
        group = group,
        pattern = { "MCPHubToolEnd", "MCPHubResourceEnd", "MCPHubPromptEnd" },
        callback = function()
            vim.g.mcphub_executing = false
        end,
    })
end

return MCPHub

---[[
--- Global state management for MCPHub
--- Handles setup, server, and UI state
---]]
local constants = require("mcphub.utils.constants")
local log = require("mcphub.utils.log")

---@class MCPHub.State
local State = {
    ---@type "not_started" | "in_progress" | "completed" | "failed" MCPHub setup state
    setup_state = "not_started",
    ---@type MCPHub.Config
    config = {},

    ---@type MCPHub.Hub?
    hub_instance = nil,
    ---@type MCPHub.UI?
    ui_instance = nil,

    -- Current hub context
    current_hub = {
        port = nil,
        workspace_root = nil,
        config_files = {}, -- Array of config files being used
        is_workspace_mode = false,
    },

    -- Config files cache
    config_files_cache = {}, -- Map of file_path -> {mcpServers, nativeMCPServers}

    -- Marketplace state
    marketplace_state = {
        ---@type "empty" | "loading" | "loaded" | "error"
        status = "empty",
        catalog = {
            ---@type MarketplaceItem[]
            items = {},
            ---@type number
            last_updated = nil,
        },
        filters = {
            search = "",
            category = "",
            sort = "stars", -- newest/stars/name
        },
        ---@type MarketplaceItem
        selected_server = nil,
        ---@type table<string, {data: table,timestamp: number}>
        server_details = {}, -- Map of mcpId -> details
    },

    -- Server state
    server_state = {
        ---@type MCPHub.Constants.HubState
        state = constants.HubState.STARTING,
        ---@type number?
        pid = nil, -- Server process ID when running
        ---@type number?
        started_at = nil, -- When server was started
        ---@type MCPServer[]
        servers = {}, -- Regular MCP servers
        ---@type NativeServer[]
        native_servers = {}, -- Native MCP servers

        ---@type MCPHub.Workspaces
        workspaces = {},
    },

    -- Error management
    errors = {
        ---@type MCPError[]
        items = {}, -- Array of error objects with type property
    },

    -- Server output
    server_output = {
        ---@type LogEntry[]
        entries = {}, -- Chronological server output entries
    },

    -- State management
    last_update = 0,
    subscribers = {
        ui = {}, -- UI-related subscribers
        server = {}, -- Server state subscribers
        all = {}, -- All state changes subscribers
        errors = {},
    },

    -- subscribers
    ---@type table<string, function[]>
    event_subscribers = {},
}

---@return boolean
function State:is_connected()
    return self.server_state.state == constants.HubState.READY
        or self.server_state.state == constants.HubState.RESTARTED
end

function State:reset()
    State.server_state = {
        status = "disconnected",
        pid = nil,
        started_at = nil,
        servers = {},
        native_servers = State.server_state.native_servers or {},
    }
    State.errors = {
        items = {},
    }
    State.server_output = {
        entries = {},
    }
    State.marketplace_state = {
        status = "loading",
        catalog = {
            items = {},
            last_updated = nil,
        },
        filters = {
            search = "",
            category = "",
            sort = "stars",
        },
        selected_server = nil,
        server_details = {},
    }
    State.last_update = 0
end

---@param partial_state table
---@param update_type? string
function State:update(partial_state, update_type)
    update_type = update_type or "all"
    local changes = {}

    -- Track changes
    for k, v in pairs(partial_state) do
        if type(v) == "table" then
            if not vim.deep_equal(self[k], v) then
                changes[k] = true
                self[k] = vim.tbl_deep_extend("force", self[k] or {}, v)
            end
        else
            if self[k] ~= v then
                changes[k] = true
                self[k] = v
            end
        end
    end

    -- Notify if changed
    if next(changes) then
        self.last_update = vim.loop.now()
        self:notify_subscribers(changes, update_type)
    end
end

--- Add an error to state and optionally log it
---@param err MCPError The error to add
---@param log_level? string Optional explicit log level (debug/info/warn/error)
function State:add_error(err, log_level)
    -- Add error to list
    table.insert(self.errors.items, err)

    -- Sort errors with newest first
    table.sort(self.errors.items, function(a, b)
        return (a.timestamp or 0) > (b.timestamp or 0)
    end)

    -- Keep reasonable history (max 100 errors)
    if #self.errors.items > 100 then
        table.remove(self.errors.items)
    end

    -- Notify subscribers
    self:notify_subscribers({
        errors = true,
    }, "errors")

    -- Log with explicit level or infer from error type
    if log_level then
        log[log_level:lower()](tostring(err))
    else
        -- Default logging behavior based on error type
        local level = err.type == "SETUP" and "error" or err.type == "SERVER" and "warn" or "info"
        log[level](tostring(err))
    end
end

--- Clear errors of a specific type or all errors
---@param type? string Optional error type to clear (setup/server/runtime)
function State:clear_errors(type)
    if type then
        -- Filter out errors of specified type
        local filtered = {}
        for _, err in ipairs(self.errors.items) do
            if err.type:lower() ~= type:lower() then
                table.insert(filtered, err)
            end
        end
        self.errors.items = filtered
    else
        -- Clear all errors
        self.errors.items = {}
    end
    self:notify_subscribers({
        errors = true,
    }, "errors")
end

function State:clear_logs()
    self.server_output.entries = {}
    self.errors.items = {}
    self:notify_subscribers({
        logs = true,
    }, "logs")
end

--- Get all errors of a specific type
---@param type? string Optional error type (setup/server/runtime)
---@return MCPError[]
function State:get_errors(type)
    if type then
        -- Filter by type
        local filtered = {}
        for _, err in ipairs(self.errors.items) do
            if err.type:lower() == type:lower() then
                table.insert(filtered, err)
            end
        end
        return vim.deepcopy(filtered)
    end
    return vim.deepcopy(self.errors.items)
end
--- Check if a server is installed by comparing id
--- @param id string Server ID to check
--- @return boolean true if server is installed
function State:is_server_installed(id)
    local servers = self.server_state.servers or {}
    for _, server in ipairs(servers) do
        if server.name == id then
            return true
        end
    end
    return false
end

---@param event string
---@param data any
function State:emit(event, data)
    local event_subscribers = self.event_subscribers[event]
    if event_subscribers then
        for _, cb in ipairs(event_subscribers) do
            cb(data)
        end
    end
end

---@param event string
---@param callback function
function State:add_event_listener(event, callback)
    self.event_subscribers[event] = self.event_subscribers[event] or {}
    table.insert(self.event_subscribers[event], callback)
end

---@param event string
---@param callback function
function State:remove_event_listener(event, callback)
    if self.event_subscribers[event] then
        for i, cb in ipairs(self.event_subscribers[event]) do
            if cb == callback then
                table.remove(self.event_subscribers[event], i)
                break
            end
        end
    end
end

---@param event string
function State:remove_all_event_listeners(event)
    self.event_subscribers[event] = {}
end

---@param entry LogEntry
function State:add_server_output(entry)
    if not entry or not entry.type or not entry.message then
        return
    end

    -- Ensure entry has timestamp
    entry.timestamp = entry.timestamp or vim.loop.now()

    table.insert(self.server_output.entries, {
        type = entry.type, -- info/warn/error/debug
        message = entry.message, -- The actual message
        timestamp = entry.timestamp,
        data = entry.data, -- Optional extra data
    })

    -- Keep reasonable history
    if #self.server_output.entries > 1000 then
        table.remove(self.server_output.entries, 1)
    end

    self:notify_subscribers({
        logs = true,
    }, "logs")
end

---@param status string
---@param ... any
function State:update_hub_state(status, ...)
    self:update({
        server_state = {
            state = status,
            unpack(... or {}),
        },
    }, "server")
    local utils = require("mcphub.utils")
    utils.fire("MCPHubStateChange", {
        state = status,
        active_servers = 0,
    })
end

---@param callback function
---@param types string[]
function State:subscribe(callback, types)
    types = types or { "all" }
    for _, type in ipairs(types) do
        self.subscribers[type] = self.subscribers[type] or {}
        table.insert(self.subscribers[type], callback)
    end
end

---@param changes table
---@param update_type string
function State:notify_subscribers(changes, update_type)
    -- Notify type-specific subscribers
    if update_type ~= "all" and self.subscribers[update_type] then
        for _, callback in ipairs(self.subscribers[update_type]) do
            callback(self, changes)
        end
    end
    -- Always notify 'all' subscribers
    for _, callback in ipairs(self.subscribers.all) do
        callback(self, changes)
    end
end

return State

---[[
--- UI Core for MCPHub
--- Handles window/buffer management and view system
---]]
local State = require("mcphub.state")
local hl = require("mcphub.utils.highlights")
local utils = require("mcphub.utils")

---@class MCPHub.UI
---@field window number Window handle
---@field buffer number Buffer handle
---@field current_view MCPHub.UI.ViewName Current view name
---@field views table Table of view instances
---@field is_shown boolean Whether the UI is currently visible
---@field cursor_states table Store cursor positions by view name
---@field context table Context from which the UI was opened
---@field opts MCPHub.UIConfig Configuration options for UI
local UI = {}
UI.__index = UI

---@enum MCPHub.UI.ViewName
local ViewName = {
    MAIN = "main",
    LOGS = "logs",
    HELP = "help",
    CONFIG = "config",
    MARKETPLACE = "marketplace",
}

-- Default window settings
---@class MCPHub.UIConfig
local defaults = {
    window = {
        width = 0.8, -- 0-1 (ratio); "50%" (percentage); 50 (raw number)
        height = 0.8, -- 0-1 (ratio); "50%" (percentage); 50 (raw number)
        align = "center", -- "center", "top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right"
        border = "rounded", -- "none", "single", "double", "rounded", "solid", "shadow"
        relative = "editor",
        zindex = 50,
    },
    ---@type table?
    wo = { -- window-scoped options (vim.wo)
    },
}

-- Parse size value into actual numbers
---@param value any Size value (number, float, or percentage string)
---@param total number Total available size
---@return number Calculated size
local function parse_size(value, total)
    if type(value) == "number" then
        if value <= 1 then -- Ratio
            return math.floor(total * value)
        end
        return math.floor(value) -- Raw number
    elseif type(value) == "string" then
        -- Parse percentage (e.g., "80%")
        local percent = tonumber(value:match("(%d+)%%"))
        if percent then
            return math.floor((total * percent) / 100)
        end
    end
    return math.floor(total * 0.8) -- Default fallback
end

--- Create a new UI instance
---@param opts? MCPHub.UIConfig Configuration options for UI
---@return MCPHub.UI
function UI:new(opts)
    local instance = {
        window = nil, -- Window handle
        buffer = nil, -- Buffer handle
        current_view = nil, -- Current view name
        views = {}, -- View instances
        is_shown = false, -- Whether the UI is currently visible
        cursor_states = {}, -- Store cursor positions by view name
        context = {}, -- Context from which the UI was opened
    }
    setmetatable(instance, self)

    self.opts = vim.tbl_deep_extend("force", defaults, opts or {})

    -- Initialize views
    instance:init_views()

    -- Subscribe to state changes
    State:subscribe(function(_, changes)
        if instance.window and vim.api.nvim_win_is_valid(instance.window) then
            -- Check if we need to update
            local should_update = false
            local keys = {
                "server_output",
                "setup_state",
                "server_state",
                "marketplace_state",
                "config_files_cache",
                "logs",
                "errors",
            }
            for k, _ in pairs(changes) do
                if vim.tbl_contains(keys, k) then
                    --if connected then only update the logs view for logs updates
                    if k == "logs" and State:is_connected() then
                        if instance.current_view == "logs" then
                            should_update = true
                        else
                            should_update = false
                        end
                    else
                        should_update = true
                    end
                    break
                end
            end
            if should_update then
                instance:render()
            end
        end
    end, { "ui", "server", "logs", "setup", "errors", "marketplace" })

    -- Create cleanup autocommands
    local group = vim.api.nvim_create_augroup("mcphub_ui", { clear = true })

    -- Handle VimLeave
    vim.api.nvim_create_autocmd("VimLeavePre", {
        group = group,
        callback = function()
            instance:cleanup()
        end,
    })

    -- Handle window resize
    vim.api.nvim_create_autocmd("VimResized", {
        group = group,
        callback = function()
            if instance.window and vim.api.nvim_win_is_valid(instance.window) then
                instance:resize_window()
            end
        end,
    })

    -- Handle window close
    vim.api.nvim_create_autocmd("WinClosed", {
        group = group,
        callback = function(args)
            if instance.window and tonumber(args.match) == instance.window then
                instance:cleanup()
            end
        end,
    })

    return instance
end

--- Initialize views
---@private
function UI:init_views()
    local MainView = require("mcphub.ui.views.main")

    -- Create view instances
    self.views = {
        main = MainView:new(self),
        logs = require("mcphub.ui.views.logs"):new(self),
        help = require("mcphub.ui.views.help"):new(self),
        config = require("mcphub.ui.views.config"):new(self),
        marketplace = require("mcphub.ui.views.marketplace"):new(self),
    }

    -- Set initial view
    self.current_view = "main"
end

--- Create a new buffer for the UI
---@private
function UI:create_buffer()
    self.buffer = vim.api.nvim_create_buf(false, true)

    -- Set buffer options
    vim.api.nvim_buf_set_option(self.buffer, "modifiable", false)
    vim.api.nvim_buf_set_option(self.buffer, "bufhidden", "wipe")
    vim.api.nvim_buf_set_option(self.buffer, "filetype", "mcphub")
    vim.api.nvim_buf_set_option(self.buffer, "wrap", true)

    -- Set buffer mappings
    self:setup_keymaps()

    return self.buffer
end

--- Calculate window dimensions and position
---@private
function UI:calculate_window_dimensions()
    local min_width = 50
    local min_height = 10
    local win_opts = self.opts.window

    -- Calculate dimensions
    local width = parse_size(win_opts.width, vim.o.columns)
    width = math.max(min_width, width)

    local height = parse_size(win_opts.height, vim.o.lines)
    height = math.max(min_height, height)

    -- Calculate position based on alignment
    local row, col
    local align = win_opts.align or "center"

    if align == "center" then
        row = math.floor((vim.o.lines - height) / 2)
        col = math.floor((vim.o.columns - width) / 2)
    elseif align == "top-left" then
        row = 1
        col = 1
    elseif align == "top-right" then
        row = 1
        col = vim.o.columns - width
    elseif align == "bottom-left" then
        row = vim.o.lines - height
        col = 1
    elseif align == "bottom-right" then
        row = vim.o.lines - height
        col = vim.o.columns - width
    elseif align == "top" then
        row = 1
        col = math.floor((vim.o.columns - width) / 2)
    elseif align == "bottom" then
        row = vim.o.lines - height
        col = math.floor((vim.o.columns - width) / 2)
    elseif align == "left" then
        row = math.floor((vim.o.lines - height) / 2)
        col = 1
    elseif align == "right" then
        row = math.floor((vim.o.lines - height) / 2)
        col = vim.o.columns - width
    else
        -- Default to center for unknown alignment
        row = math.floor((vim.o.lines - height) / 2)
        col = math.floor((vim.o.columns - width) / 2)
    end

    return {
        width = width,
        height = height,
        row = row,
        col = col,
    }
end

--- Resize the window when editor is resized
---@private
function UI:resize_window()
    if not self.window or not vim.api.nvim_win_is_valid(self.window) then
        return
    end
    vim.api.nvim_win_set_config(self.window, self:get_window_config())
    -- Force a re-render of the current view
    self:render()
end

function UI:get_window_config()
    local dims = self:calculate_window_dimensions()
    local win_opts = self.opts.window

    return {
        relative = win_opts.relative,
        width = dims.width,
        height = dims.height,
        row = dims.row,
        col = dims.col,
        style = "minimal",
        border = win_opts.border,
        zindex = win_opts.zindex,
    }
end

--- Create a floating window
---@private
---@return number Window handle
function UI:create_window()
    if not self.buffer or not vim.api.nvim_buf_is_valid(self.buffer) then
        self:create_buffer()
    end

    -- Create floating window
    self.window = vim.api.nvim_open_win(self.buffer, true, self:get_window_config())

    for k, v in pairs(self.opts.wo or {}) do
        vim.api.nvim_set_option_value(k, v, { scope = "local", win = self.window })
    end

    return self.window
end

--- Set up view-specific keymaps
function UI:setup_keymaps()
    local function map(key, action, desc)
        vim.keymap.set("n", key, action, {
            buffer = self.buffer,
            desc = desc,
            nowait = true,
        })
    end

    -- Global navigation
    map("H", function()
        self:switch_view("main")
    end, "Switch to Home view")
    map("M", function()
        self:switch_view("marketplace")
    end, "Switch to Marketplace")

    map("C", function()
        self:switch_view("config")
    end, "Switch to Config view")

    map("L", function()
        self:switch_view("logs")
    end, "Switch to Logs view")

    map("?", function()
        self:switch_view("help")
    end, "Switch to Help view")

    -- Close window
    map("q", function()
        self:cleanup()
    end, "Close")

    map("r", function()
        self:hard_refresh()
    end, "Refresh")
    map("R", function()
        self:restart()
    end, "Restart")
end

function UI:refresh()
    if State.hub_instance then
        vim.notify("Refreshing")
        if State.hub_instance:refresh() then
            vim.notify("Refreshed")
        else
            vim.notify("Failed to refresh")
        end
    else
        vim.notify("No hub instance available")
    end
end

function UI:restart()
    if State.hub_instance then
        State.hub_instance:restart(function(success)
            if success then
                vim.notify("Restarting...")
            else
                vim.notify("Failed to restart")
            end
        end)
        vim.schedule(function()
            self:switch_view("main")
        end)
    else
        vim.notify("No hub instance available")
    end
end

function UI:hard_refresh()
    if State.hub_instance then
        vim.notify("Updating all server capabilities")
        State.hub_instance:hard_refresh(function(success)
            if success then
                vim.notify("Refreshed")
            else
                vim.notify("Failed to refresh")
            end
        end)
    else
        vim.notify("No hub instance available")
    end
end

--- Clean up resources
function UI:cleanup()
    if not (self.window and vim.api.nvim_win_is_valid(self.window)) then
        return
    end

    -- Clean up buffer if it exists
    if self.buffer and vim.api.nvim_buf_is_valid(self.buffer) then
        vim.api.nvim_buf_delete(self.buffer, { force = true })
    end

    -- Close window if it exists
    if self.window and vim.api.nvim_win_is_valid(self.window) then
        vim.api.nvim_win_close(self.window, true)
    end
    self.buffer = nil
    self.window = nil
    self.is_shown = false
end

--- Toggle UI visibility
--- @param args? table
function UI:toggle(args)
    if self.window and vim.api.nvim_win_is_valid(self.window) then
        self:cleanup()
    else
        self:show(args)
    end
end

--- Switch to a different view
---@param view_name MCPHub.UI.ViewName Name of view to switch to
function UI:switch_view(view_name)
    -- Leave current view if any
    if self.current_view and self.views[self.current_view] and self.is_shown then
        self.views[self.current_view]:before_leave()
        self.views[self.current_view]:after_leave()
    end

    -- Switch view
    self.current_view = view_name

    -- Enter new view
    if self.views[view_name] then
        self.views[view_name]:before_enter()
        self.views[view_name]:draw()
        self.views[view_name]:after_enter()
    end
end

--- Show the UI window
--- @param args? table
function UI:show(args)
    self.context = utils.get_buf_info(vim.api.nvim_get_current_buf(), args)
    -- Create/show window if needed
    if not self.window or not vim.api.nvim_win_is_valid(self.window) then
        self:create_window()
    end
    -- Focus window
    vim.api.nvim_set_current_win(self.window)

    -- Draw current view
    self:render()
    self.is_shown = true
end

--- Render current view
---@private
function UI:render()
    self:switch_view(self.current_view)
end

return UI

---@diagnostic disable: undefined-doc-name, return-type-mismatch, undefined-field
--[[ Adapted from nui.line by Munif Tanjim
Source: https://github.com/MunifTanjim/nui.nvim/blob/main/lua/nui/line/init.lua
License: MIT ]]
local NuiText = require("mcphub.utils.nuitext")

---@class NuiLine
local Line = {}
Line.__index = Line

---@class NuiLine
---@field _texts NuiText[]

---@param texts? NuiText[]
function Line:init(texts)
    self._texts = texts or {}
end

---@param content string|NuiText|NuiLine
---@param highlight? string|nui_text_extmark highlight info
---@return NuiText|NuiLine
function Line:append(content, highlight)
    local block = content
    if block == nil then
        return self
    end
    if type(block) == "string" then
        block = NuiText(block, highlight)
    end
    if block._texts then
        ---@cast block NuiLine
        for _, text in ipairs(block._texts) do
            table.insert(self._texts, text)
        end
    else
        ---@cast block NuiText
        table.insert(self._texts, block)
    end
    return self
end

---@return string
function Line:content()
    return table.concat(vim.tbl_map(function(text)
        return text:content()
    end, self._texts))
end

---@return number
function Line:width()
    local width = 0
    for _, text in ipairs(self._texts) do
        width = width + text:width()
    end
    return width
end

---@param bufnr number buffer number
---@param ns_id number namespace id
---@param linenr number line number (1-indexed)
---@param char_start? number start character position (0-indexed)
function Line:highlight(bufnr, ns_id, linenr, char_start)
    local current_byte_start = char_start or 0
    for _, text in ipairs(self._texts) do
        text:highlight(bufnr, ns_id, linenr, current_byte_start)
        current_byte_start = current_byte_start + text:length()
    end
end

---@param bufnr number buffer number
---@param ns_id number namespace id
---@param linenr_start number start line number (1-indexed)
---@param linenr_end? number end line number (1-indexed)
function Line:render(bufnr, ns_id, linenr_start, linenr_end)
    local row_start = linenr_start - 1
    local row_end = linenr_end and linenr_end - 1 or row_start + 1
    local content = self:content()
    --handle newlines
    content = content:gsub("\n", "\\n")

    -- if vim.bo[bufnr].modifiable == false then
    --     vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr })
    -- end
    -- Clear existing content
    vim.api.nvim_buf_set_lines(bufnr, row_start, row_end, false, { "" })
    -- Insert new content at column 0
    vim.api.nvim_buf_set_text(bufnr, row_start, 0, row_start, 0, { content })
    -- Highlight from column 0
    self:highlight(bufnr, ns_id, linenr_start, 0)
end

-- Create a new line instance
local function new_line(...)
    local instance = setmetatable({}, Line)
    instance:init(...)
    return instance
end

-- Static methods

---@param content string text content
---@param width number total width
---@param align? "left"|"center"|"right" text alignment
---@param highlight? string highlight group
---@return NuiLine
function Line.pad_text(content, width, align, highlight)
    local text_width = vim.api.nvim_strwidth(content)
    local padding = width - text_width

    if padding <= 0 then
        return new_line():append(content, highlight)
    end

    local line = new_line()
    local left_pad, right_pad = 0, 0

    if align == "right" then
        left_pad = padding
    elseif align == "center" then
        left_pad = math.floor(padding / 2)
        right_pad = padding - left_pad
    else -- left or default
        right_pad = padding
    end

    if left_pad > 0 then
        line:append(string.rep(" ", left_pad))
    end
    line:append(content, highlight)
    if right_pad > 0 then
        line:append(string.rep(" ", right_pad))
    end

    return line
end

---@param parts string[] text parts
---@param highlight? string highlight group
---@param separator? string separator between parts
---@return NuiLine
function Line.join(parts, highlight, separator)
    local line = new_line()
    separator = separator or " "

    for i, part in ipairs(parts) do
        if i > 1 then
            line:append(separator)
        end
        line:append(part, highlight)
    end

    return line
end

-- Create constructor
local NuiLine = setmetatable({
    pad_text = Line.pad_text, -- Export static methods
    join = Line.join,
}, {
    __call = function(_, ...)
        return new_line(...)
    end,
})

return NuiLine

---@brief [[
--- Utility functions for generating MCP system prompts.
--- Parts of the prompts are inspired from RooCode repository:
--- https://github.com/RooVetGit/Roo-Code
---@brief ]]
local M = {}
local State = require("mcphub.state")
local config_manager = require("mcphub.utils.config_manager")
local log = require("mcphub.utils.log")
local native = require("mcphub.native")
local utils = require("mcphub.utils")
local validation = require("mcphub.utils.validation")

---@return string
local function get_header()
    return [[
# MCP SERVERS

The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
]]
end

---@param server_name string
---@param header? string
---@return string
function M.format_custom_instructions(server_name, header)
    local server_config = config_manager.get_server_config(server_name) or {}
    local custom_instructions = server_config.custom_instructions or {}

    if custom_instructions.text and custom_instructions.text ~= "" and not custom_instructions.disabled then
        return string.format(
            ((header and header ~= "") and header or "\n\n#### Instructions for `%s` server\n\n")
                .. custom_instructions.text,
            server_name
        )
    end
    return ""
end

---@param def MCPServer|NativeServer|MCPTool|MCPResource|MCPResourceTemplate
---@return string
function M.get_description(def)
    local description = def.description or ""
    if type(description) == "function" then
        local ok, desc = pcall(description, def)
        if not ok then
            description = "Failed to get description :" .. (desc or "")
        else
            description = desc or ""
        end
    end
    return description
end

---@param def MCPTool
---@return table
function M.get_inputSchema(def)
    local base = {
        type = "object",
        properties = {},
    }
    local inputSchema = def.inputSchema
    if not inputSchema or (type(inputSchema) == "table" and not next(inputSchema)) then
        inputSchema = base
    end
    local parsedSchema = inputSchema
    if type(parsedSchema) == "function" then
        local ok, schema = pcall(parsedSchema, def)
        if not ok then
            local err = "Error in inputSchema function: " .. tostring(schema)
            log.error(err)
            parsedSchema = base
        else
            parsedSchema = schema or base
        end
    end
    local res = validation.validate_inputSchema(parsedSchema, def.name)
    if not res.ok then
        local err = "Error in inputSchema function: " .. tostring(res.error)
        log.error(err)
        return base
    end
    return parsedSchema
end

---@param tools MCPTool[]
---@return string
local function format_tools(tools)
    if not tools or #tools == 0 then
        return ""
    end

    local result = "\n\n#### Available Tools"
    for _, tool in ipairs(tools) do
        result = result .. string.format("\n\n- %s: %s", tool.name, M.get_description(tool):gsub("\n", "\n  "))
        local inputSchema = M.get_inputSchema(tool)
        result = result
            .. "\n\n  Input Schema:\n\n  ```json\n  "
            .. utils.pretty_json(vim.json.encode(inputSchema)):gsub("\n", "\n  ")
            .. "\n  ```"
    end
    return result
end

---@diagnostic disable-next-line: unused-function
local function remove_functions(obj)
    if type(obj) ~= "table" then
        return obj
    end
    local new_obj = {}
    for k, v in pairs(obj) do
        if type(v) ~= "function" then
            new_obj[k] = remove_functions(v)
        end
    end
    return new_obj
end

---@param resources MCPResource[]
---@param templates MCPResourceTemplate[]
---@return string
local function format_resources(resources, templates)
    local result = ""
    if resources and #resources > 0 then
        result = result .. "\n\n#### Available Resources"
        for _, resource in ipairs(resources) do
            result = result
                .. string.format(
                    "\n\n- %s%s",
                    resource.uri,
                    resource.mimeType and " (" .. resource.mimeType .. ")" or ""
                )
            local desc = M.get_description(resource):gsub("\n", "\n  ")
            result = result .. "\n  " .. (resource.name or "") .. (desc == "" and "" or "\n  " .. desc)
            -- result = result .. "\n\n" .. vim.inspect(remove_functions(resource))
        end
    end
    if templates and #templates > 0 then
        result = result .. "\n\n#### Available Resource Templates"
        for _, template in ipairs(templates) do
            result = result .. string.format("\n\n- %s", template.uriTemplate)
            local desc = M.get_description(template)
            result = result .. "\n  " .. (template.name or "") .. (desc == "" and "" or "\n  " .. desc)
            -- result = result .. "\n\n" .. vim.inspect(remove_functions(template))
        end
    end
    return result
end

--- Get the use_mcp_tool section of the prompt
---@param example? string Optional custom XML example block
---@return string The formatted prompt section
function M.get_use_mcp_tool_prompt(example)
    local default_example = [[<use_mcp_tool>
<server_name>weather-server</server_name>
<tool_name>get_forecast</tool_name>
<arguments>
{
  "city": "San Francisco",
  "days": 5
}
</arguments>
</use_mcp_tool>]]

    return string.format(
        [[
### use_mcp_tool

Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.
Parameters:
- server_name: (required) The name of the MCP server providing the tool
- tool_name: (required) The name of the tool to execute
- arguments: (required) A JSON object containing the tool's input parameters, following the tool's input schema

Example: Requesting to use an MCP tool

%s]],
        example or default_example
    )
end

--- Get the access_mcp_resource section of the prompt
---@param example? string Optional custom XML example block
---@return string The formatted prompt section
function M.get_access_mcp_resource_prompt(example)
    local default_example = [[<access_mcp_resource>
<server_name>weather-server</server_name>
<uri>weather://san-francisco/current</uri>
</access_mcp_resource>]]

    return string.format(
        [[
### access_mcp_resource

Description: Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information.
Parameters:
- server_name: (required) The name of the MCP server providing the resource
- uri: (required) The URI identifying the specific resource to access

Example: Requesting to access an MCP resource

%s]],
        example or default_example
    )
end

---@param server MCPServer
---@return string
function M.server_to_text(server)
    local text = ""
    -- Add server section
    text = text .. string.format("### %s", server.name)
    local is_disabled = server.status == "disabled"
    if is_disabled then
        text = text .. " (Disabled)"
    end
    local desc = M.get_description(server)
    -- Add description
    text = text .. (desc == "" and "" or "\n\n" .. desc)
    if is_disabled then
        return text
    end
    if
        server.capabilities
        and (
            (server.capabilities.tools and #server.capabilities.tools > 0)
            or (server.capabilities.resources and #server.capabilities.resources > 0)
            or (server.capabilities.resourceTemplates and #server.capabilities.resourceTemplates > 0)
        )
    then
        -- Add custom instructions if any
        text = text .. M.format_custom_instructions(server.name)

        -- Add capabilities
        text = text .. format_tools(server.capabilities.tools)
        text = text .. format_resources(server.capabilities.resources, server.capabilities.resourceTemplates)
    else
        text = text .. "\n(No tools or resources available)"
    end
    return text
end

---@param servers MCPServer[]
---@param add_example boolean
---@param enable_toggling_mcp_servers boolean
---@return string
function M.get_active_servers_prompt(servers, add_example, enable_toggling_mcp_servers)
    add_example = add_example ~= false
    enable_toggling_mcp_servers = enable_toggling_mcp_servers ~= false
    local prompt = get_header()

    ---@type MCPServer[]
    local connected_servers = vim.tbl_filter(function(s)
        return s.status == "connected"
    end, servers)

    ---@type MCPServer[]
    local disabled_servers = vim.tbl_filter(function(s)
        return s.status == "disabled"
    end, servers)

    prompt = prompt .. "\n## Connected MCP Servers"

    prompt = prompt
        .. "\n\nWhen a server is connected, you can use the server's tools via the `use_mcp_tool` tool, "
        .. "and access the server's resources via the `access_mcp_resource` tool.\nNote: Server names are case sensitive and you should always use the exact full name like `Firecrawl MCP` or `src/user/main/time-mcp` etc\n\n"
    if #connected_servers == 0 then
        prompt = prompt .. "(No connected MCP servers)\n\n"
    else
        for _, server in ipairs(connected_servers) do
            prompt = prompt .. M.server_to_text(server) .. "\n\n"
        end
    end

    -- instead of removing the whole disabled section, if we dont want auto toggling we set (NO disabled servers) to avoid llm hallucinating server names
    prompt = prompt .. "## Disabled MCP Servers\n\n"
    prompt = prompt
        .. "When a server is disabled, it will not be able to provide tools or resources. You can start one of the following disabled servers by using the `toggle_mcp_server` tool on `mcphub` MCP Server if it is connected using `use_mcp_tool`\n\n"
    if not enable_toggling_mcp_servers or #disabled_servers == 0 then
        prompt = prompt .. "(No disabled MCP servers)\n\n"
    else
        for _, server in ipairs(disabled_servers) do
            prompt = prompt .. M.server_to_text(server) .. "\n\n"
        end
    end
    local toggle_example = [[

### Toggling a MCP Server

When you need to start a disabled MCP Server or vice-versa, use the `toggle_mcp_server` tool on `mcphub` MCP Server using `use_mcp_tool`:

CRITICAL: You need to use the `use_mcp_tool` tool to call the `toggle_mcp_server` tool on `mcphub` MCP Server when `mcphub` server is "Connected" else ask the user to enable `mcphub` server.

Pseudocode:

use_mcp_tool
  server_name: "mcphub"
  tool_name: "toggle_mcp_server"
  tool_input:
    server_name: string (One of the available server names to start or stop)
    action: string (one of `start` or `stop`)
]]

    local example = [[
## Examples

### `use_mcp_tool`

When you need to call a tool on an MCP Server, use the `use_mcp_tool` tool:

Pseudocode:

use_mcp_tool
  server_name: string (One of the available server names)
  tool_name: string (name of the tool in the server to call)
  tool_input: object (Arguments for the tool call)

### `access_mcp_resource`

When you need to access a resource from a MCP Server, use the `access_mcp_resource` tool:

Pseudocode:

access_mcp_resource
  server_name: string (One of the available server names)
  uri: string (uri for the resource)
]]

    if enable_toggling_mcp_servers then
        example = example .. toggle_example
    end

    return prompt .. (add_example and example or "")
end

---@param response {result: {messages: {role:string, content: table}[]}}|nil
---@return {messages : {role:"user"| "assistant"|"system", output: MCPResponseOutput}[]}
function M.parse_prompt_response(response)
    if response == nil then
        return { messages = {} }
    end
    local result = response.result or {}
    local messages = {}
    for _, v in ipairs(result.messages or {}) do
        local output = { text = "", images = {}, blobs = {}, audios = {} }
        local content = v.content
        if content.type == "text" then
            output.text = content.text
        elseif content.type == "image" then
            table.insert(output.images, {
                data = content.data,
                mimeType = content.mimeType or "application/octet-stream",
            })
        elseif content.type == "audio" then
            table.insert(output.audios, {
                data = content.data,
                mimeType = content.mimeType or "application/octet-stream",
            })
        elseif content.type == "blob" then
            table.insert(output.blobs, {
                data = content.data,
                mimeType = content.mimeType or "application/octet-stream",
            })
        elseif content.type == "resource" then
            -- Handle resource content by treating it as a resource response
            local resource_result = M.parse_resource_response({
                result = {
                    contents = { content.resource },
                },
            })
            output.text = resource_result.text
            vim.list_extend(output.images, resource_result.images)
            vim.list_extend(output.blobs, resource_result.blobs)
            vim.list_extend(output.audios, resource_result.audios)
        end
        table.insert(messages, {
            role = v.role,
            output = output,
        })
    end
    return {
        messages = messages,
    }
end

---@param response? table
---@return MCPResponseOutput
function M.parse_tool_response(response)
    if response == nil then
        return { error = nil, text = "", images = {}, blobs = {}, audios = {} }
    end

    local result = response.result or {}
    local output = { text = "", images = {}, blobs = {}, audios = {}, error = nil }
    local images = {}
    local blobs = {}
    local texts = {}
    local audios = {}

    -- parse tool response
    for _, v in ipairs(result.content or {}) do
        local type = v.type
        if type == "text" then
            table.insert(texts, v.text)
        elseif type == "image" then
            table.insert(images, {
                data = v.data,
                mimeType = v.mimeType or "application/octet-stream",
            })
        elseif type == "blob" then
            table.insert(output.blobs, {
                data = v.data,
                mimeType = v.mimeType or "application/octet-stream",
            })
        elseif type == "audio" then
            table.insert(audios, {
                data = v.data,
                mimeType = v.mimeType or "application/octet-stream",
            })
        elseif type == "resource" and v.resource then
            -- Handle resource content by treating it as a resource response
            local resource_result = M.parse_resource_response({
                result = {
                    contents = { v.resource },
                },
            })
            -- Merge the results
            table.insert(texts, resource_result.text)
            vim.list_extend(images, resource_result.images)
            vim.list_extend(blobs, resource_result.blobs)
            vim.list_extend(audios, resource_result.audios)
        end
    end

    -- Combine all text with newlines
    output.text = table.concat(texts, "\n")
    if result.isError then
        output.error = output.text
        output.text = "The tool run failed with error.\n" .. output.text
    end
    output.images = images
    output.blobs = blobs
    output.audios = audios

    return output
end

---@param response? table
---@return MCPResponseOutput
function M.parse_resource_response(response)
    if response == nil then
        return { text = "", images = {}, blobs = {} }
    end

    local result = response.result or {}
    local output = { text = "", images = {}, blobs = {} }
    local images = {}
    local blobs = {}
    local texts = {}
    local audios = {}

    for _, content in ipairs(result.contents or {}) do
        if content.uri then
            if content.blob then
                -- Handle blob data based on mimetype
                if content.mimeType and content.mimeType:match("^image/") then
                    -- It's an image
                    table.insert(images, {
                        data = content.blob,
                        mimeType = content.mimeType,
                    })
                elseif content.mimeType and content.mimeType:match("^audio/") then
                    -- It's an audio blob
                    table.insert(audios, {
                        data = content.blob,
                        mimeType = content.mimeType,
                    })
                else
                    -- It's a binary blob
                    table.insert(blobs, {
                        data = content.blob,
                        mimeType = content.mimeType or "application/octet-stream",
                        uri = content.uri,
                    })
                    -- Add blob info to text
                    table.insert(
                        texts,
                        string.format(
                            "Resource %s: <Binary data of type %s>",
                            content.uri,
                            content.mimeType or "application/octet-stream"
                        )
                    )
                end
            elseif content.text then
                -- Text content
                table.insert(texts, string.format("Resource %s:\n%s", content.uri, content.text))
            end
        end
    end

    output.text = table.concat(texts, "\n\n")
    output.images = images
    output.blobs = blobs
    output.audios = audios
    return output
end

--- Get the troubleshooting guide for MCPHub
--- @return string|nil The troubleshooting guide content or nil if not found
function M.get_troubleshooting_guide()
    local source_path = debug.getinfo(1, "S").source:sub(2) -- Remove '@' prefix
    local base_path = vim.fn.fnamemodify(source_path, ":h:h:h:h") -- Go up three levels from prompt.lua
    local guide_path = base_path .. "/doc/other/troubleshooting.md"
    local f = io.open(guide_path)
    if not f then
        return nil
    end

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

    return content
end

--- Get the native server creation prompt from the guide file
---@return string|nil The native server guide content or nil if not found
function M.get_native_server_prompt()
    -- Use source file path to find the guide
    local source_path = debug.getinfo(1, "S").source:sub(2) -- Remove '@' prefix
    local base_path = vim.fn.fnamemodify(source_path, ":h:h") -- Go up three levels from prompt.lua
    local guide_path = base_path .. "/native/NATIVE_SERVER_LLM.md"
    local f = io.open(guide_path)
    if not f then
        return nil
    end

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

    return content
end

--- Get mcphub.nvim plugin documentation intended for llms
---@return string|nil The plugin docs content or nil if not found
function M.get_plugin_docs()
    local source_path = debug.getinfo(1, "S").source:sub(2)
    local base_path = vim.fn.fnamemodify(source_path, ":h:h:h:h")
    local guide_path = base_path .. "/doc/mcphub.txt"
    local f = io.open(guide_path)
    if not f then
        return nil
    end
    local content = f:read("*all")
    f:close()
    return content
end

--- Get the changelog for the mcphub.nvim plugin
--- @return string|nil The changelog content or nil if not found
function M.get_plugin_changelog()
    local source_path = debug.getinfo(1, "S").source:sub(2)
    local base_path = vim.fn.fnamemodify(source_path, ":h:h:h:h")
    local guide_path = base_path .. "/CHANGELOG.md"
    local f = io.open(guide_path)
    if not f then
        return nil
    end
    local content = f:read("*all")
    f:close()
    return content
end

return M

local M = {}
local core = require("mcphub.extensions.codecompanion.core")

local mcphub = require("mcphub")

--- Utility functions for naming
---@param name string
---@return string
local function make_safe_name(name)
    name = name:gsub("[^%w_]", "_")
    return name
end

---@param server_name string Name of the MCP server
---@param tool_name string Tool name
---@return string
local function create_namespaced_tool_name(server_name, tool_name)
    local safe_server_name = make_safe_name(server_name)
    local safe_tool_name = make_safe_name(tool_name)
    return safe_server_name .. "__" .. safe_tool_name
end

--- Create handler for static tools (use_mcp_tool, access_mcp_resource)
---@param action_name MCPHub.ActionType
---@param has_function_calling boolean
---@param opts MCPHub.Extensions.CodeCompanionConfig
local function create_static_handler(action_name, has_function_calling, opts)
    ---@param agent CodeCompanion.Agent The Editor tool
    ---@param args MCPHub.ToolCallArgs | MCPHub.ResourceAccessArgs The arguments from the LLM's tool call
    ---@param output_handler function Callback for asynchronous calls
    ---@return nil|{ status: "success"|"error", data: string }
    return function(agent, args, _, output_handler)
        local context = {
            tool_display_name = action_name,
            is_individual_tool = false,
            action = action_name,
        }
        core.execute_mcp_tool(args, agent, output_handler, context)
    end
end

---@class MCPHub.ToolCallContext
---@field tool_display_name string
---@field is_individual_tool boolean
---@field action MCPHub.ActionType

--- Create handler for individual tools
---@param server_name string MCP Server name
---@param tool_name string Tool name on the server
---@param namespaced_name string Namespaced tool name (safe_server_name__safe_tool_name)
---@return function
local function create_individual_tool_handler(server_name, tool_name, namespaced_name)
    ---@param agent CodeCompanion.Agent The Editor tool
    ---@param args MCPHub.ToolCallArgs
    ---@param output_handler function Callback for asynchronous calls
    return function(agent, args, _, output_handler)
        local params = {
            server_name = server_name,
            tool_name = tool_name,
            tool_input = args,
        }
        ---@type MCPHub.ToolCallContext
        local context = {
            tool_display_name = namespaced_name,
            is_individual_tool = true,
            action = "use_mcp_tool",
        }
        core.execute_mcp_tool(params, agent, output_handler, context)
    end
end

-- Static tool schemas
local tool_schemas = {
    access_mcp_resource = {
        type = "function",
        ["function"] = {
            name = "access_mcp_resource",
            description = "get resources on MCP servers.",
            parameters = {
                type = "object",
                properties = {
                    server_name = {
                        description = "Name of the server to call the resource on. Must be from one of the available servers.",
                        type = "string",
                    },
                    uri = {
                        description = "URI of the resource to access.",
                        type = "string",
                    },
                },
                required = { "server_name", "uri" },
                additionalProperties = false,
            },
            strict = true,
        },
    },
    use_mcp_tool = {
        type = "function",
        ["function"] = {
            name = "use_mcp_tool",
            description = "calls tools on MCP servers.",
            parameters = {
                type = "object",
                properties = {
                    server_name = {
                        description = "Name of the server to call the tool on. Must be from one of the available servers.",
                        type = "string",
                    },
                    tool_name = {
                        description = "Name of the tool to call.",
                        type = "string",
                    },
                    tool_input = {
                        description = "Input object for the tool call",
                        type = "object",
                    },
                },
                required = { "server_name", "tool_name", "tool_input" },
                additionalProperties = false,
            },
            strict = false,
        },
    },
}

--- Create static MCP tools
---@param opts MCPHub.Extensions.CodeCompanionConfig
---@return {groups: table<string, table>, [MCPHub.ActionType]: table}
function M.create_static_tools(opts)
    local codecompanion = require("codecompanion")
    local has_function_calling = codecompanion.has("function-calling") --[[@as boolean]]

    local tools = {
        groups = {
            mcp = {
                id = "mcp_static:mcp",
                description = " Call tools and resources from MCP servers with:\n\n - `use_mcp_tool`\n - `access_mcp_resource`\n",
                hide_in_help_window = false,
                system_prompt = function(_)
                    local hub = require("mcphub").get_hub_instance()
                    if not hub then
                        vim.notify("MCP Hub is not initialized", vim.log.levels.WARN)
                        return ""
                    end
                    if not hub:is_ready() then
                        vim.notify("MCP Hub is not ready yet", vim.log.levels.WARN)
                        return ""
                    end
                    local prompt = ""
                    if not has_function_calling then
                        local xml_tool = require("mcphub.extensions.codecompanion.xml_tool")
                        prompt = xml_tool.system_prompt(hub)
                    end
                    prompt = prompt .. hub:get_active_servers_prompt()
                    return prompt
                end,
                tools = {},
                opts = {
                    collapse_tools = true,
                },
            },
        },
    }

    for action_name, schema in pairs(tool_schemas) do
        tools[action_name] = {
            id = "mcp_static:" .. action_name,
            description = schema["function"].description,
            hide_in_help_window = true,
            visible = false,
            ---@class MCPHub.Extensions.CodeCompanionTool: CodeCompanion.Agent.Tool
            callback = {
                name = action_name,
                cmds = { create_static_handler(action_name, has_function_calling, opts) },
                system_prompt = function()
                    return string.format("You can use the %s tool to %s\n", action_name, schema["function"].description)
                end,
                output = core.create_output_handlers(action_name, has_function_calling, opts),
                schema = schema,
            },
        }
        table.insert(tools.groups.mcp.tools, action_name)
    end

    return tools
end

-- Cleanup dynamic tools and groups
local function cleanup_dynamic_items(config)
    local tools = config.strategies.chat.tools
    local groups = tools.groups or {}

    -- Clean up existing MCP dynamic tools
    for key, value in pairs(tools) do
        local id = value.id or ""
        if id:sub(1, 11) == "mcp_dynamic" then
            tools[key] = nil
        end
    end

    -- Clean up existing MCP dynamic tool groups
    for key, value in pairs(groups) do
        local id = value.id or ""
        if id:sub(1, 11) == "mcp_dynamic" then
            groups[key] = nil
        end
    end
end

---@param opts MCPHub.Extensions.CodeCompanionConfig
function M.register(opts)
    local hub = mcphub.get_hub_instance()
    if not hub then
        return
    end

    local ok, config = pcall(require, "codecompanion.config")
    if not ok then
        return
    end

    -- Cleanup existing dynamic items
    cleanup_dynamic_items(config)

    local tools = config.strategies.chat.tools
    local groups = tools.groups or {}

    -- Get servers and process in one go
    local servers = hub:get_servers()
    local server_tools = {} -- Map safe_server_name -> {tool_names, server_name}
    local used_safe_names = {}
    local skipped_tools = {}
    local skipped_groups = {}

    -- Process servers: create unique safe names and individual tools
    for _, server in ipairs(servers) do
        local safe_name = make_safe_name(server.name)
        local counter = 1
        local original_safe_name = safe_name

        -- Ensure unique safe name
        while used_safe_names[safe_name] do
            safe_name = original_safe_name .. "_" .. counter
            counter = counter + 1
        end

        used_safe_names[safe_name] = true

        if opts.add_mcp_prefix_to_tool_names then
            safe_name = "mcp__" .. safe_name
        end
        -- Check if this safe_name conflicts with existing group
        if groups[safe_name] then
            table.insert(skipped_groups, safe_name)
            -- Skip this entire server to avoid confusing individual tools
            goto continue
        end

        server_tools[safe_name] = { tool_names = {}, server_name = server.name }

        -- Create individual tools for this server
        if server.capabilities and server.capabilities.tools then
            for _, tool in ipairs(server.capabilities.tools) do
                local tool_name = tool.name
                local namespaced_tool_name = create_namespaced_tool_name(safe_name, tool_name)
                -- Check for tool name conflicts (after cleanup, no mcp_dynamic should exist)
                if tools[namespaced_tool_name] then
                    table.insert(skipped_tools, namespaced_tool_name)
                else
                    -- Track for server group
                    table.insert(server_tools[safe_name].tool_names, namespaced_tool_name)

                    -- Add individual tool
                    tools[namespaced_tool_name] = {
                        id = "mcp_dynamic:" .. safe_name .. ":" .. tool_name,
                        description = tool.description,
                        hide_in_help_window = true,
                        visible = opts.show_server_tools_in_chat == true,
                        callback = {
                            name = namespaced_tool_name,
                            cmds = { create_individual_tool_handler(server.name, tool_name, namespaced_tool_name) },
                            output = core.create_output_handlers(namespaced_tool_name, true, opts),
                            schema = {
                                type = "function",
                                ["function"] = {
                                    name = namespaced_tool_name,
                                    description = tool.description,
                                    parameters = tool.inputSchema,
                                },
                            },
                        },
                    }
                end
            end
        end

        ::continue::
    end

    -- Create server groups
    local prompt_utils = require("mcphub.utils.prompt")
    for safe_server_name, server_data in pairs(server_tools) do
        local tool_names = server_data.tool_names
        local server_name = server_data.server_name

        -- Only create group if it has tools and no conflict
        if #tool_names > 0 then
            if groups[safe_server_name] then
                table.insert(skipped_groups, safe_server_name)
            else
                local custom_instructions = prompt_utils.format_custom_instructions(
                    server_name,
                    "\n\n### Instructions for " .. safe_server_name .. " tools\n\n"
                )

                groups[safe_server_name] = {
                    id = "mcp_dynamic:" .. safe_server_name,
                    hide_in_help_window = true,
                    description = string.format(
                        " All tools from `%s` MCP server: \n\n%s",
                        server_name,
                        table.concat(
                            vim.tbl_map(function(t)
                                return " - `" .. t .. "` "
                            end, tool_names),
                            "\n"
                        )
                    ),
                    tools = tool_names,
                    system_prompt = function(self)
                        if custom_instructions and custom_instructions ~= "" then
                            return custom_instructions
                        end
                    end,
                    opts = {
                        collapse_tools = true,
                    },
                }
            end
        end
    end

    -- Silent warnings for conflicts
    if #skipped_tools > 0 then
        vim.notify(
            string.format(
                "Skipped adding %d tool(s) to codecompanion due to name conflicts: %s",
                #skipped_tools,
                table.concat(skipped_tools, ", ")
            ),
            vim.log.levels.WARN,
            { title = "MCPHub" }
        )
    end

    if #skipped_groups > 0 then
        vim.notify(
            string.format(
                "Skipped adding %d server group(s) to codecompanion due to name conflicts: %s",
                #skipped_groups,
                table.concat(skipped_groups, ", ")
            ),
            vim.log.levels.WARN,
            { title = "MCPHub" }
        )
    end

    -- Update syntax highlighting
    M.update_syntax_highlighting(server_tools)
end

--- Setup dynamic tools (individual tools + server groups)
---@param opts MCPHub.Extensions.CodeCompanionConfig
function M.setup_dynamic_tools(opts)
    if not opts.make_tools then
        return
    end
    vim.schedule(function()
        M.register(opts)
    end)
    mcphub.on(
        { "servers_updated", "tool_list_changed" },
        vim.schedule_wrap(function()
            M.register(opts)
        end)
    )
end

-- Update syntax highlighting for new tools
function M.update_syntax_highlighting(server_tools)
    vim.schedule(function()
        for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
            if vim.api.nvim_buf_is_valid(bufnr) and vim.bo[bufnr].filetype == "codecompanion" then
                vim.api.nvim_buf_call(bufnr, function()
                    for safe_server_name, server_data in pairs(server_tools) do
                        local tool_names = server_data.tool_names
                        vim.cmd.syntax('match CodeCompanionChatToolGroup "@{' .. safe_server_name .. '}"')
                        vim.iter(tool_names):each(function(name)
                            vim.cmd.syntax('match CodeCompanionChatTool "@{' .. name .. '}"')
                        end)
                    end
                end)
            end
        end
    end)
end

return M

local Error = require("mcphub.utils.errors")
local State = require("mcphub.state")
local log = require("mcphub.utils.log")
local utils = require("mcphub.utils")
local validation = require("mcphub.utils.validation")

local M = {}

--- Find server by name in current active servers
--- @param server_name string
--- @param from_non_native? boolean Whether to search in non-native servers
--- @return table|nil server object with config_source field
local function find_server_by_name(server_name, from_non_native)
    -- Search in native servers
    if from_non_native ~= true then
        for _, server in ipairs(State.server_state.native_servers or {}) do
            if server.name == server_name then
                return vim.tbl_extend("force", server, { is_native = true })
            end
        end
    end
    -- Search in regular MCP servers
    for _, server in ipairs(State.server_state.servers or {}) do
        if server.name == server_name then
            return server
        end
    end
    return nil
end

--- Read and parse a config file
--- @param file_path string Path to the config file
--- @return table|nil config Parsed config or nil on error
--- @return string|nil error Error message if parsing failed
local function read_config_file(file_path)
    -- Check if file exists, create with default content if not
    local file = io.open(file_path, "r")
    if not file then
        -- Ensure parent directory exists
        local dir_path = vim.fn.fnamemodify(file_path, ":h")
        if vim.fn.isdirectory(dir_path) == 0 then
            vim.fn.mkdir(dir_path, "p")
        end

        local create_file = io.open(file_path, "w")
        if not create_file then
            return nil, "Failed to create config file: " .. file_path
        end

        -- Write default config
        local default_config = { mcpServers = vim.empty_dict() }
        create_file:write(vim.json.encode(default_config))
        create_file:close()

        -- Reopen for reading
        file = io.open(file_path, "r")
        if not file then
            return nil, "Failed to reopen created config file: " .. file_path
        end
    end

    -- Read and parse JSON
    local content = file:read("*a")
    file:close()

    local success, json = utils.json_decode(content, { use_custom_parser = true })
    if not success then
        local error_msg = [[Invalid JSON in config file: ]]
            .. file_path
            .. [[

If your config file uses JSON5 syntax (comments, trailing commas), please:
1. Install lua-json5: https://github.com/Joakker/lua-json5
2. Add to your mcphub setup: json_decode = require('json5').parse

Example:
  require('mcphub').setup({
    json_decode = require('json5').parse,
    -- other config...
  })

Parse error: ]]
            .. tostring(json)
        return nil, error_msg
    end

    return json, nil
end

--- Detect original format and normalize config
--- @param config table Raw config object
--- @param file_path string File path for logging
--- @return table normalized_config Config with mcpServers key
--- @return string original_servers_key Original key used ("servers" or "mcpServers")
--- @return string|nil error Error message if validation failed
local function normalize_config_format(config, file_path)
    -- Format detection
    local has_servers = config.servers and type(config.servers) == "table"
    local has_mcp_servers = config.mcpServers and type(config.mcpServers) == "table"
    local original_servers_key = "mcpServers" -- default

    if not has_servers and not has_mcp_servers then
        return nil, nil, "Config file must contain either 'servers' or 'mcpServers' object: " .. file_path
    end

    local normalized = vim.deepcopy(config)

    -- Determine original format and normalize
    if has_servers and not has_mcp_servers then
        -- Pure VS Code format
        original_servers_key = "servers"
        normalized.mcpServers = config.servers
        log.debug("ConfigManager: VS Code format detected in " .. file_path)
    elseif has_servers and has_mcp_servers then
        -- Both exist, prefer servers (VS Code format takes precedence)
        original_servers_key = "servers"
        normalized.mcpServers = config.servers
        log.debug("ConfigManager: Both 'servers' and 'mcpServers' found in " .. file_path .. ", using 'servers'")
    end
    return normalized, original_servers_key, nil
end

--- Validate all servers in a config
--- @param config table Config with mcpServers
--- @param file_path string File path for error reporting
--- @return boolean success True if all servers are valid
--- @return string|nil error Error message if validation failed
local function validate_servers_in_config(config, file_path)
    for server_name, server_config in pairs(config.mcpServers or {}) do
        local validation_result = validation.validate_server_config(server_name, server_config)
        if not validation_result.ok then
            return false,
                "Server validation failed for '"
                    .. server_name
                    .. "' in "
                    .. file_path
                    .. ": "
                    .. validation_result.error.message
        end
    end
    return true, nil
end

--- Write config back to file preserving original format
--- @param config table Config to write
--- @param file_path string Path to write to
--- @param original_servers_key string Original key format ("servers" or "mcpServers")
--- @return boolean success True if write succeeded
--- @return string|nil error Error message if write failed
local function write_config_file(config, file_path, original_servers_key)
    local output_config = vim.deepcopy(config)

    -- Convert back to original format if needed
    if original_servers_key == "servers" and output_config.mcpServers then
        output_config.servers = output_config.mcpServers
        output_config.mcpServers = nil
    end

    local utils = require("mcphub.utils")
    local json_str = utils.pretty_json(vim.json.encode(output_config), { use_jq = true })

    local file = io.open(file_path, "w")
    if not file then
        return false, "Failed to open config file for writing: " .. file_path
    end

    file:write(json_str)
    file:close()
    return true, nil
end

--- Read, parse, normalize and validate a config file - returns table with all info
--- @param file_path string Path to the config file
--- @return table result {ok: boolean, json: table?, content: string?, original_key: string?, error: string?}
local function validate_config_file(file_path)
    -- Read raw config
    local raw_config, read_error = read_config_file(file_path)
    if not raw_config then
        return {
            ok = false,
            error = read_error,
        }
    end

    -- Normalize format and detect original key
    local normalized_config, original_servers_key, normalize_error = normalize_config_format(raw_config, file_path)
    if not normalized_config then
        return {
            ok = false,
            error = normalize_error,
        }
    end

    -- Validate all servers
    local valid, validation_error = validate_servers_in_config(normalized_config, file_path)
    if not valid then
        return {
            ok = false,
            error = validation_error,
        }
    end

    return {
        ok = true,
        json = normalized_config,
        original_json = raw_config,
        original_key = original_servers_key,
    }
end

--- Load a single config file and cache it
--- @param file_path string Path to the config file
--- @return boolean success Whether the file was loaded successfully
--- @return string|nil error Error message if loading failed
function M.load_config(file_path)
    if not file_path or file_path == "" then
        log.warn("ConfigManager: Empty file path provided")
        return false, "Empty file path provided"
    end

    local file_result = validate_config_file(file_path)
    if not file_result.ok then
        log.error("ConfigManager: " .. file_result.error)
        return false, file_result.error
    end

    -- Cache the normalized config, ensuring empty objects stay as objects
    State.config_files_cache = State.config_files_cache or {}
    State.config_files_cache[file_path] = {
        mcpServers = file_result.json.mcpServers or vim.empty_dict(),
        nativeMCPServers = file_result.json.nativeMCPServers or vim.empty_dict(),
    }
    vim.schedule(function()
        State:notify_subscribers({ config_files_cache = true }, "ui")
    end)

    log.debug("ConfigManager: Loaded config file: " .. file_path .. " (format: " .. file_result.original_key .. ")")
    return true, nil
end

--- Get the config source for a server
--- @param server string|table Server name or server object
--- @return string|nil Config source path or nil if not found
function M.get_config_source(server)
    local server_obj

    if type(server) == "string" then
        -- Find server in active servers list
        server_obj = find_server_by_name(server)
        if not server_obj then
            log.debug("ConfigManager: Server not found: " .. server)
            return nil
        end
    else
        -- server is an object
        server_obj = server
    end
    return server_obj.config_source or State.config.config
end

--- Get server config by server name or server object
--- @param server string|table Server name or server object with config_source
--- @param from_non_native? boolean Whether to get from non-native servers
--- @return table|nil Server config or nil if not found
function M.get_server_config(server, from_non_native)
    local server_name, config_source, is_native

    if type(server) == "string" then
        -- Find server in active servers list to get config_source
        local server_obj = find_server_by_name(server, from_non_native)
        if not server_obj then
            log.debug("ConfigManager: Server not found: " .. server)
            return nil
        end
        server_name = server
        config_source = server_obj.config_source
        is_native = server_obj.is_native or false
    else
        -- server is an object
        server_name = server.name
        config_source = server.config_source
        is_native = server.is_native or false
    end

    if not config_source then
        log.warn("ConfigManager: No config_source for server: " .. server_name)
        return nil
    end

    -- Ensure config is loaded
    if not State.config_files_cache or not State.config_files_cache[config_source] then
        log.debug("ConfigManager: Config not cached, loading: " .. config_source)
        if not M.load_config(config_source) then
            return nil
        end
    end

    -- Get config from cache
    local file_config = State.config_files_cache[config_source]
    if is_native then
        return file_config.nativeMCPServers[server_name]
    else
        return file_config.mcpServers[server_name]
    end
end

--- Update server config in the correct source file
--- @param server string|table Server name or server object
--- @param config table | nil New config to merge/replace
--- @param options? { merge : boolean?, config_source: string?} Options
--- @return boolean success Whether the update was successful
function M.update_server_config(server, config, options)
    options = options or {}
    local merge = options.merge ~= false -- default to true

    local server_name, config_source, is_native

    if type(server) == "string" then
        -- Use {} so that we can add a new server
        local server_obj = find_server_by_name(server) or {}
        server_name = server
        -- For servers without config_source, or new servers use global config
        config_source = server_obj.config_source or State.config.config
        is_native = server_obj.is_native or false
    else
        server_name = server.name
        if not server_name then
            log.error("ConfigManager: Invalid server object provided, missing name")
            return false
        end
        config_source = server.config_source or State.config.config
        is_native = server.is_native or false
    end
    if options.config_source then
        config_source = options.config_source
    end
    if not config_source then
        log.error("ConfigManager: No config_source for server: " .. server_name)
        return false
    end

    -- Load current config file
    local file_result = validate_config_file(config_source)
    if not file_result.ok then
        log.error(
            "ConfigManager: Failed to load config file for update: " .. config_source .. " - " .. file_result.error
        )
        return false
    end

    local file_config = file_result.json
    local original_servers_key = file_result.original_key
    local server_section = is_native and "nativeMCPServers" or "mcpServers"

    -- Ensure section exists
    file_config[server_section] = file_config[server_section] or {}

    -- Update server config
    if merge and type(config) == "table" and file_config[server_section][server_name] then
        file_config[server_section][server_name] =
            vim.tbl_deep_extend("force", file_config[server_section][server_name], config)
    else
        file_config[server_section][server_name] = config
    end

    -- Write back to file preserving original format
    local write_success, write_error = write_config_file(file_config, config_source, original_servers_key)
    if not write_success then
        log.error("ConfigManager: " .. write_error)
        return false
    end

    -- Update cache
    State.config_files_cache = State.config_files_cache or {}
    State.config_files_cache[config_source] = {
        mcpServers = file_config.mcpServers or {},
        nativeMCPServers = file_config.nativeMCPServers or {},
    }
    State:notify_subscribers({
        config_files_cache = true,
    }, "ui")

    log.debug("ConfigManager: Updated server config: " .. server_name .. " in " .. config_source)
    return true
end

--- Refresh config cache from files in current hub context
--- Uses State.current_hub.config_files to know which files to reload
--- @param paths string[]|nil Array of file paths to refresh, if nil uses current hub context
--- @return boolean success Whether all files were refreshed successfully
--- @return string|nil error Error message if any file failed to refresh
function M.refresh_config(paths)
    if not paths and (not State.current_hub or not State.current_hub.config_files) then
        log.debug("ConfigManager: No current hub context for refresh")
        return false, "No current hub context for refresh"
    end

    State.config_files_cache = {} -- Clear cache
    paths = paths or State.current_hub.config_files

    for _, file_path in ipairs(paths) do
        local ok, error_msg = M.load_config(file_path)
        if not ok then
            return false, "Failed to load config file '" .. file_path .. "': " .. (error_msg or "unknown error")
        end
    end

    log.debug("ConfigManager: Refreshed " .. #paths .. " config files")
    return true, nil
end

--- Get all active config files from current hub context
--- @param reverse? boolean Whether to reverse the order of files
--- @return string[] Array of config file paths
function M.get_active_config_files(reverse)
    if not State.current_hub or not State.current_hub.config_files then
        return {}
    end
    local config_files = vim.deepcopy(State.current_hub.config_files)
    if reverse then
        return vim.fn.reverse(config_files)
    end
    return State.current_hub.config_files
end

--- Get the raw config content for a specific file (for UI display)
--- @param file_path string Path to the config file
--- @return table|nil config content or nil if not found/invalid
function M.get_file_config(file_path)
    -- Ensure file is loaded in cache
    if not State.config_files_cache or not State.config_files_cache[file_path] then
        if not M.load_config(file_path) then
            return nil
        end
    end

    return State.config_files_cache[file_path]
end

--- Get the full config file content as JSON string (for Config UI)
--- @param file_path string Path to the config file
--- @return table|nil JSON content or nil if not found
--- @return string|nil error Error message if loading failed
function M.get_file_content_json(file_path)
    local file_result = validate_config_file(file_path)
    if not file_result.ok then
        return nil, file_result.error
    end
    return file_result.original_json, nil
end

--- Initialize config cache with current hub context
--- Should be called when hub context changes
--- @return boolean success
function M.initialize()
    log.debug("ConfigManager: Initializing with current hub context")
    return M.refresh_config()
end

return M

local Error = require("mcphub.utils.errors")
local State = require("mcphub.state")
local constants = require("mcphub.utils.constants")
local log = require("mcphub.utils.log")
local utils = require("mcphub.utils")

local M = {}

local function format_config_changes(changes)
    local removed = changes.removed or {}
    local added = changes.added or {}
    local modified = changes.modified or {}
    local msg = ""
    if #added > 0 then
        msg = msg .. "Added:\n"
        for _, item in ipairs(added) do
            msg = msg .. string.format("  - %s\n", item)
        end
    end
    if #modified > 0 then
        msg = msg .. "Modified:\n"
        for _, item in ipairs(modified) do
            msg = msg .. string.format("  - %s\n", item)
        end
    end
    if #removed > 0 then
        msg = msg .. "Removed:\n"
        for _, item in ipairs(removed) do
            msg = msg .. string.format("  - %s\n", item)
        end
    end
    return msg
end

-- SSE Event handlers
M.SSEHandlers = {
    ---@param event string Event type
    ---@param data table Event data
    ---@param hub MCPHub.Hub Hub instance
    handle_sse_event = function(event, data, hub)
        local is_ui_shown = State.ui_instance and State.ui_instance.is_shown or false
        if event == constants.EventTypes.HEARTBEAT then
            log.trace("Heartbeat event received")
        else
            log.debug(
                string.format(
                    "Event: %s %s ",
                    event,
                    (data.type or data.state) and ": " .. (data.type or data.state) or ""
                )
            )
        end
        if event == constants.EventTypes.HUB_STATE then
            local state = data.state
            if state then
                State:update_hub_state(state)
            end
            if state == constants.HubState.ERROR then
                hub:handle_hub_stopped("Hub entered error state: " .. (data.message or "unknown error"))
            elseif state == constants.HubState.STOPPING then
                hub:handle_hub_stopping()
            elseif state == constants.HubState.STOPPED then
                hub:handle_hub_stopped("Hub stopped")
            elseif state == constants.HubState.RESTARTING then
                hub:handle_hub_restarting()
            elseif state == constants.HubState.READY or state == constants.HubState.RESTARTED then
                hub:handle_hub_ready()
            end
        elseif event == constants.EventTypes.SUBSCRIPTION_EVENT then
            local is_capability_type = vim.tbl_contains({
                constants.SubscriptionTypes.TOOL_LIST_CHANGED,
                constants.SubscriptionTypes.RESOURCE_LIST_CHANGED,
                constants.SubscriptionTypes.PROMPT_LIST_CHANGED,
            }, data.type)
            if is_capability_type then
                hub:handle_capability_updates(data)
            elseif data.type == constants.SubscriptionTypes.CONFIG_CHANGED then
                -- if not is_ui_shown then
                --     local has_significant_changes = data.isSignificant == true
                --     if not has_significant_changes then
                --         vim.notify("MCP Hub Config Changed: No Significant changes found", vim.log.levels.INFO, {
                --             title = "MCP Hub",
                --         })
                --     end
                -- end
                hub:reload_config()
            elseif data.type == constants.SubscriptionTypes.SERVERS_UPDATING then
                if not is_ui_shown then
                    vim.notify("MCP Hub Config Changed", vim.log.levels.INFO)
                end
                hub:refresh()
            elseif data.type == constants.SubscriptionTypes.SERVERS_UPDATED then
                if not is_ui_shown then
                    vim.notify(
                        "MCP Servers Updated:\n\n" .. format_config_changes(data.changes or {}),
                        vim.log.levels.INFO
                    )
                end
                hub:refresh()
            elseif data.type == constants.SubscriptionTypes.WORKSPACES_UPDATED then
                hub:refresh()
            end
        elseif event == constants.EventTypes.LOG then
            -- Use message timestamp if valid ISO string, otherwise system time
            local timestamp = vim.loop.now()
            if data.timestamp then
                -- Try to convert ISO string to unix timestamp
                local success, ts = pcall(function()
                    return vim.fn.strptime("%Y-%m-%dT%H:%M:%S", data.timestamp)
                end)
                if success then
                    timestamp = ts
                end
            end
            State:add_server_output({
                type = data.type,
                message = data.message,
                code = data.code,
                timestamp = timestamp,
                data = data.data,
            })
            -- Handle errors specially
            if data.type == "error" then
                local error_obj = Error("SERVER", data.code or Error.Types.SERVER.CONNECTION, data.message, data.data)
                State:add_error(error_obj)
            end
        end
    end,
}

-- Parameter type handlers for validation and conversion
M.TypeHandlers = {
    string = {
        validate = function(value)
            return type(value) == "string"
        end,
        convert = function(value)
            return tostring(value)
        end,
        format = function()
            return "string"
        end,
    },
    number = {
        validate = function(value)
            return tonumber(value) ~= nil
        end,
        convert = function(value)
            return tonumber(value)
        end,
        format = function()
            return "number"
        end,
    },
    integer = {
        validate = function(value)
            local num = tonumber(value)
            return num and math.floor(num) == num
        end,
        convert = function(value)
            return math.floor(tonumber(value) and tonumber(value) or 0)
        end,
        format = function()
            return "integer"
        end,
    },
    boolean = {
        validate = function(value)
            return value == "true" or value == "false"
        end,
        convert = function(value)
            return value == "true"
        end,
        format = function()
            return "boolean"
        end,
    },
    object = {
        validate = function(value, schema)
            -- Parse JSON object string and validate each property
            -- FIXME: need to implement proper validation for objects
            local ok, obj = utils.json_decode(value)
            if not ok or type(obj) ~= "table" then
                return false
            end
            return true
        end,
        format = function(schema)
            if schema.properties then
                local props = {}
                for k, v in pairs(schema.properties) do
                    if v.type then
                        local type_handler = M.TypeHandlers[v.type]
                        table.insert(
                            props,
                            string.format("%s: %s", k, type_handler and type_handler.format(v) or v.type)
                        )
                    elseif v.anyOf then
                        table.insert(
                            props,
                            string.format(
                                "%s: anyOf(%s)",
                                k,
                                vim.iter(v.anyOf)
                                    :map(function(item)
                                        return vim.inspect(item.type or "unknown")
                                    end)
                                    :join(",")
                            )
                        )
                    else
                        table.insert(props, string.format("%s: %s", k, "unknown"))
                    end
                end
                return string.format("{%s}", table.concat(props, ", "))
            end
            return "object"
        end,
        convert = function(value)
            local ok, result = utils.json_decode(value)
            return ok and result or {}
        end,
    },
    array = {
        validate = function(value, schema)
            -- Parse JSON array string and validate each item
            local ok, arr = utils.json_decode(value)
            if not ok or type(arr) ~= "table" then
                return false
            end
            -- If items has enum, validate against allowed values
            if schema.items and schema.items.enum then
                for _, item in ipairs(arr) do
                    if not vim.tbl_contains(schema.items.enum, item) then
                        return false
                    end
                end
            end
            -- If items has type, validate each item's type
            if schema.items and schema.items.type then
                local item_validator = M.TypeHandlers[schema.items.type].validate
                for _, item in ipairs(arr) do
                    if not item_validator(item, schema.items) then
                        return false
                    end
                end
            end
            return true
        end,
        convert = function(value)
            local ok, result = utils.json_decode(value)
            return ok and result or {}
        end,
        format = function(schema)
            if schema.items then
                if schema.items.enum then
                    return string.format(
                        "[%s]",
                        table.concat(
                            vim.tbl_map(function(v)
                                return string.format("%q", v)
                            end, schema.items.enum),
                            ", "
                        )
                    )
                elseif schema.items.type then
                    return string.format("%s[]", M.TypeHandlers[schema.items.type].format(schema.items))
                end
            end
            return "array"
        end,
    },
}

--- API response handlers
M.ResponseHandlers = {
    --- Process API errors and create structured error objects
    --- @param error table|string Error from API
    --- @param context? table Additional context to include
    --- @return MCPError Structured error object
    process_error = function(error, context)
        if type(error) == "table" then
            if error.code and error.message then
                -- Already structured error
                if context then
                    error.data = vim.tbl_extend("force", error.data or {}, context)
                end
                return Error("SERVER", Error.Types.SERVER.API_ERROR, vim.inspect(error), context)
            end
            -- Table error without proper structure
            return Error("SERVER", Error.Types.SERVER.API_ERROR, vim.inspect(error), context)
        end
        -- String error
        return Error("SERVER", Error.Types.SERVER.API_ERROR, error, context)
    end,

    --- Handle curl specific errors
    --- @param response table Curl response
    --- @param context table Request context
    --- @return MCPError|nil error Structured error if any
    handle_curl_error = function(response, context)
        if not response then
            return Error("SERVER", Error.Types.SERVER.CURL_ERROR, "No response from server", context)
        end

        if response.exit ~= 0 then
            local error_code = ({
                [7] = Error.Types.SERVER.CONNECTION,
                [28] = Error.Types.SERVER.TIMEOUT,
            })[response.exit] or Error.Types.SERVER.CURL_ERROR

            local error_msg = ({
                [7] = "Connection refused - Server not running",
                [28] = "Request timed out",
            })[response.exit] or string.format("Request failed (code %d)", response.exit or 0)

            return Error(
                "SERVER",
                error_code,
                error_msg,
                vim.tbl_extend("force", context, {
                    exit_code = response.exit,
                })
            )
        end

        return nil
    end,

    --- Handle HTTP error responses
    --- @param response table HTTP response
    --- @param context table Request context
    --- @return MCPError|nil error Structured error if any
    handle_http_error = function(response, context)
        if response.status < 400 then
            return nil
        end

        local ok, parsed_error = utils.json_decode(response.body)
        if ok and parsed_error.error then
            return Error(
                "SERVER",
                parsed_error.code or Error.Types.SERVER.API_ERROR,
                parsed_error.error,
                parsed_error.data or {}
            )
        end

        return Error(
            "SERVER",
            Error.Types.SERVER.API_ERROR,
            string.format("Server error (%d)", response.status),
            vim.tbl_extend("force", context, {
                status = response.status,
                body = response.body,
            })
        )
    end,

    --- Parse JSON response
    --- @param response string Raw response body
    --- @param context table Request context
    --- @return table|nil result Parsed response or nil
    --- @return MCPError|nil error Structured error if any
    parse_json = function(response, context)
        if not response then
            return nil, Error("SERVER", Error.Types.SERVER.API_ERROR, "Empty response from server", context)
        end

        local ok, decoded = utils.json_decode(response)
        if not ok then
            return nil,
                Error(
                    "SERVER",
                    Error.Types.SERVER.API_ERROR,
                    "Invalid response: Not JSON",
                    vim.tbl_extend("force", context, {
                        body = response,
                    })
                )
        end

        return decoded, nil
    end,
}

return M

local issue_tracker = require("mcphub.native.neovim.files.edit_file.issue_tracker")

---@class DiffParser
---@field config ParserConfig Configuration options
---@field tracker IssueTracker Issue tracking instance
local DiffParser = {}
DiffParser.__index = DiffParser

-- Default parser configuration
local DEFAULT_CONFIG = {
    track_issues = true, -- Track parsing issues for LLM feedback
    extract_inline_content = true, -- Handle content on marker lines
}

-- Create new diff parser instance
---@param config ParserConfig? Optional configuration
---@return DiffParser
function DiffParser.new(config)
    local self = setmetatable({}, DiffParser)
    self.config = vim.tbl_deep_extend("force", DEFAULT_CONFIG, config or {})
    self.tracker = issue_tracker.new()
    return self
end

-- Parse diff string into structured blocks
---@param diff_content string Raw diff content from LLM
---@return ParsedBlock[]? blocks Parsed blocks (nil on error)
---@return string? error_message Error message if parsing failed
function DiffParser:parse(diff_content)
    if not diff_content or diff_content == "" then
        return nil, "Empty diff content"
    end

    -- Clear previous issues
    self.tracker:clear()

    -- Normalize and fix malformed content
    local normalized_diff = self:_normalize_content(diff_content)
    local fixed_diff = self:_fix_malformed_diff(normalized_diff)

    -- Parse the diff into blocks
    local blocks, error = self:_parse_blocks(fixed_diff)
    if error then
        return nil, error
    end

    return blocks, nil
end

-- Check if any issues were found
---@return boolean has_issues
function DiffParser:has_issues()
    return self.tracker:has_issues()
end

-- Clear tracked issues
function DiffParser:clear_issues()
    self.tracker:clear()
end

-- Normalize line endings and basic cleanup
---@param diff_content string Raw diff content
---@return string normalized_content
function DiffParser:_normalize_content(diff_content)
    -- Normalize line endings
    local text = diff_content:gsub("\r\n", "\n"):gsub("\r", "\n")
    return text
end

-- Fix common malformed diff patterns from LLMs
---@param diff_content string Normalized diff content
---@return string fixed_content
function DiffParser:_fix_malformed_diff(diff_content)
    local lines = vim.split(diff_content, "\n")

    -- Check if we have any search marker at all
    local has_search_marker = false
    for _, line in ipairs(lines) do
        if self:_detect_marker_type(line) == "search" then
            has_search_marker = true
            break
        end
    end

    if has_search_marker then
        return diff_content
    end

    self.tracker:track_issue("MALFORMED_SEARCH_MARKER")

    -- Handle markdown code blocks
    local first_line = lines[1]
    if first_line and first_line:match("^%s*```") then
        table.insert(lines, 2, "<<<<<<< SEARCH")
        if self.config.track_issues then
            self.tracker:track_issue("MARKDOWN_NOISE")
        end
    else
        table.insert(lines, 1, "<<<<<<< SEARCH")
    end

    return table.concat(lines, "\n")
end

-- Parse fixed diff content into blocks
---@param diff_content string Fixed diff content
---@return ParsedBlock[]? blocks Parsed blocks (nil on error)
---@return string? error_message Error message if parsing failed
function DiffParser:_parse_blocks(diff_content)
    local lines = vim.split(diff_content, "\n")
    local blocks = {}
    local current_search = {}
    local current_replace = {}
    local state = "waiting" -- "waiting", "searching", "replacing"
    local block_counter = 1

    for line_num, line in ipairs(lines) do
        local marker_type, inline_content = self:_detect_marker_type(line, true)

        if marker_type == "search" then
            if state ~= "waiting" then
                return nil,
                    "Unexpected SEARCH marker at line "
                        .. line_num
                        .. " - expected SEPARATOR or REPLACE marker. If content to search or replace for contains <<<<<<< SEARCH, please escape it with a backslash like \\<<<<<<< SEARCH. SEARCH marker must be the first line of a block."
            end
            -- Start of search block
            state = "searching"
            current_search = {}

            -- Handle inline content on search marker line
            if inline_content and self.config.extract_inline_content then
                table.insert(current_search, inline_content)
            end
        elseif marker_type == "separator" then
            if state ~= "searching" then
                return nil,
                    "Unexpected separator at line "
                        .. line_num
                        .. " - expected SEARCH marker. If content to search or replace for contains ======, please escape it with a backslash like \\======. Separator must be between SEARCH and REPLACE markers."
            end
            -- Transition from search to replace
            state = "replacing"
            current_replace = {}
        elseif marker_type == "replace" then
            if state ~= "replacing" then
                return nil,
                    "Unexpected REPLACE marker at line "
                        .. line_num
                        .. " - expected REPLACE marker. If content to search or replace for contains >>>>>>> REPLACE, please escape it with a backslash like \\>>>>>> REPLACE. REPLACE marker must follow a SEARCH marker and a SEPARATOR."
            end
            -- End of replace block - create block
            state = "waiting"

            -- Handle inline content on replace marker line
            if inline_content and self.config.extract_inline_content then
                table.insert(current_replace, inline_content)
            end

            -- Create the block
            local search_content = table.concat(current_search, "\n")
            local replace_content = table.concat(current_replace, "\n")

            -- Skip empty blocks (unless it's intentional deletion)
            if search_content ~= "" or replace_content ~= "" then
                local block = {
                    search_content = search_content,
                    replace_content = replace_content,
                    block_id = "Block " .. block_counter,
                    search_lines = vim.deepcopy(current_search),
                    replace_lines = vim.deepcopy(current_replace),
                }
                table.insert(blocks, block)
                block_counter = block_counter + 1
            end
        elseif state == "searching" then
            -- Collect search content
            table.insert(current_search, self:unescape_markers(line))
        elseif state == "replacing" then
            -- Collect replace content
            table.insert(current_replace, self:unescape_markers(line))
        end
    end

    -- Handle incomplete blocks
    if state == "searching" and #current_search > 0 then
        return nil, "Incomplete search block - missing separator or replace section"
    end

    if state == "replacing" then
        return nil, "Incomplete replace block - missing replace marker"
    end

    if #blocks == 0 then
        return nil, "No valid SEARCH/REPLACE blocks found"
    end

    return blocks, nil
end

-- Detect marker type and extract inline content
---@param line string Line to analyze
---@param track boolean Track issues if true
---@return string? marker_type "search", "separator", "replace", or nil
---@return string? inline_content Content found on marker line
function DiffParser:_detect_marker_type(line, track)
    -- Search marker: minimum 5 < chars, optional spaces, case-insensitive SEARCH
    local search_arrows, search_spaces, search_keyword, search_content =
        line:match("^%s*(<<<<<+)(%s*)([Ss][Ee][Aa][Rr][Cc][Hh])(.*)$")

    if search_arrows then
        local inline_content = nil

        if self.config.extract_inline_content and search_content and search_content:match("%S") then
            -- Handle claude-4 case: "<<<<<<< SEARCH> content", use .- to handle cases with just >. If not the inline content becomes `>` and the first file line will be matched with >
            local claude_content = search_content:match("^>%s*(.-)$")
            if claude_content then
                if claude_content ~= "" then
                    inline_content = claude_content
                end
                if track then
                    self.tracker:track_issue("CLAUDE_MARKER_ISSUE", { inline_content = claude_content, line = line })
                end
            else
                -- Regular content after marker
                local regular_content = search_content:match("^%s*(.+)$")
                if regular_content and regular_content ~= "" then
                    inline_content = regular_content
                end
                if track then
                    self.tracker:track_issue("CONTENT_ON_MARKER_LINE", { inline_content = inline_content, line = line })
                end
            end
        end

        -- Track spacing issues
        if track then
            if search_spaces == "" then
                self.tracker:track_issue("MISSING_SPACES_IN_MARKERS")
            elseif #search_spaces > 1 then
                self.tracker:track_issue("EXTRA_SPACES_IN_MARKERS")
            end
            -- Track case issues
            if search_keyword ~= "SEARCH" then
                self.tracker:track_issue("CASE_MISMATCH_MARKERS")
            end
        end

        return "search", inline_content
    end

    -- Replace marker: minimum 5 > chars
    local replace_arrows, replace_spaces, replace_keyword, replace_trailing =
        line:match("^%s*(>>>>>+)(%s*)([Rr][Ee][Pp][Ll][Aa][Cc][Ee])(.*)$")

    if replace_arrows then
        local inline_content = nil

        -- Check for content AFTER the marker (trailing content)
        if self.config.extract_inline_content and replace_trailing and replace_trailing:match("%S") then
            inline_content = replace_trailing:match("^%s*(.-)%s*$") -- trim spaces
        end

        -- Track spacing and case issues
        if track then
            if replace_spaces == "" then
                self.tracker:track_issue("MISSING_SPACES_IN_MARKERS")
            elseif #replace_spaces > 1 then
                self.tracker:track_issue("EXTRA_SPACES_IN_MARKERS")
            end

            if replace_keyword ~= "REPLACE" then
                self.tracker:track_issue("CASE_MISMATCH_MARKERS")
            end
        end

        return "replace", inline_content
    end

    if line:match("^=======%s*$") then
        return "separator", nil
    end

    return nil, nil
end

-- Get comprehensive feedback including statistics
---@return string? feedback Complete feedback for LLM
function DiffParser:get_feedback()
    local parsing_feedback = self.tracker:get_llm_feedback()
    if parsing_feedback then
        return "## ISSUES WHILE PARSING DIFF\n" .. parsing_feedback
    end
end

function DiffParser:unescape_markers(line)
    local newline = line:gsub("^%s*\\<<<<<", "<<<<<<"):gsub("^%s*\\=====", "====="):gsub("^%s*\\>>>>>", ">>>>>")
    newline = newline:gsub("^%s*\\\\<<<<<", "<<<<<<"):gsub("^%s*\\\\=====", "====="):gsub("^%s*\\\\>>>>>", ">>>>>")
    return newline
end

return DiffParser

-- Search Engine for Block Location

local string_utils = require("mcphub.native.neovim.files.edit_file.string_utils")
local types = require("mcphub.native.neovim.files.edit_file.types")

---@class UsedRange
---@field start_line integer Start of used range
---@field end_line integer End of used range

---@class SearchResult
---@field score number Overall match score (0.0-1.0)
---@field start_line integer Starting line position
---@field line_details LineMatchDetail[] Per-line match details
---@field match_type OverallMatchType Type of match found

---@class SearchEngine
---@field config table Configuration options
---@field used_ranges UsedRange[] Track used line ranges to handle duplicate blocks
local SearchEngine = {}
SearchEngine.__index = SearchEngine

-- Default configuration
local DEFAULT_CONFIG = {
    fuzzy_threshold = 0.8, -- Minimum similarity score for fuzzy matches
    enable_fuzzy_matching = true, -- Allow fuzzy matching when exact fails
    early_termination_score = 1, -- Stop searching when match is this good
    max_search_iterations = 10000, -- Prevent runaway searches
}

-- Create new search engine instance
---@param config table? Optional configuration
---@return SearchEngine
function SearchEngine.new(config)
    local self = setmetatable({}, SearchEngine)
    self.config = vim.tbl_deep_extend("force", DEFAULT_CONFIG, config or {})
    self.used_ranges = {} -- Track used line ranges for duplicate handling
    return self
end

-- Reset used ranges (call this when starting a new file)
function SearchEngine:reset_used_ranges()
    self.used_ranges = {}
end

-- Main search function - top-to-bottom linear approach
---@param search_lines string[] Lines to search for
---@param file_lines string[] All file lines
---@return BlockLocationResult
function SearchEngine:locate_block_in_file(search_lines, file_lines)
    if #search_lines == 0 then
        return {
            found = true,
            start_line = 1,
            end_line = 1,
            overall_score = 1,
            overall_match_type = types.OVERALL_MATCH_TYPE.exact,
            confidence = 100,
            found_content = table.concat(file_lines, "\n"),
            found_lines = file_lines,
            line_details = {},
            search_metadata = {},
        }
    end
    -- Perform top-to-bottom linear search
    return self:_linear_search(search_lines, file_lines)
end

-- Linear top-to-bottom search that handles exact, whitespace, and fuzzy matches
---@param search_lines string[] Lines to search for
---@param file_lines string[] All file lines
---@return BlockLocationResult
function SearchEngine:_linear_search(search_lines, file_lines)
    local search_len = #search_lines
    local total_lines = #file_lines

    -- Validate bounds
    if total_lines < search_len then
        return {
            found = false,
            error = "SEARCH block lines more than file lines. If you are rewriting the file, make sure the SEARCH block is empty",
        }
    end

    ---@type SearchResult
    local best_result = {
        score = 0,
        start_line = -1,
        line_details = {},
        match_type = types.OVERALL_MATCH_TYPE.fuzzy_low, -- Default to lowest type
    }

    local iterations = 0
    local early_termination = false

    -- Search from top to bottom
    for start_pos = 1, total_lines - search_len + 1 do
        iterations = iterations + 1

        -- Check if this position is available (not used by previous blocks)
        if self:_is_position_available(start_pos, search_len) then
            local result = self:_evaluate_position(search_lines, file_lines, start_pos)

            -- For exact matches, take the first one found (top-to-bottom priority)
            if result.match_type == types.OVERALL_MATCH_TYPE.exact then
                best_result = result
                break
            end

            -- For fuzzy matches, keep track of the best one so far
            if self:_is_better_result(result, best_result) then
                best_result = result
            end
        end

        -- Prevent runaway searches
        if iterations >= self.config.max_search_iterations then
            break
        end
    end

    -- Check if we found a suitable match
    local min_score = self.config.enable_fuzzy_matching and self.config.fuzzy_threshold or 0.99
    if
        best_result.score >= min_score
        or best_result.match_type == types.OVERALL_MATCH_TYPE.exact
        or best_result.match_type == types.OVERALL_MATCH_TYPE.exact_whitespace
    then
        -- Mark this range as used
        self:_mark_range_used(best_result.start_line, best_result.start_line + search_len - 1)

        return self:_create_success_result(best_result, search_lines, file_lines)
    else
        return {
            found = false,
            error = "No suitable match found",
            overall_score = best_result.score,
            confidence = math.floor(best_result.score * 100),
            start_line = best_result.start_line,
            end_line = best_result.start_line + search_len - 1,
            found_content = table.concat(
                vim.tbl_map(function(
                    line --[[@as LineMatchDetail]]
                )
                    return line.found_line
                end, best_result.line_details),
                "\n"
            ),
        }
    end
end

-- Check if a position is available (not already used)
---@param start_line integer Starting position to check
---@param search_len integer Length of the search block
---@return boolean available True if position is available
function SearchEngine:_is_position_available(start_line, search_len)
    local end_line = start_line + search_len - 1

    -- Early exit if no used ranges
    if #self.used_ranges == 0 then
        return true
    end

    for _, used_range in ipairs(self.used_ranges) do
        -- Check for overlap: ranges overlap if NOT (a.end < b.start OR a.start > b.end)
        if not (end_line < used_range.start_line or start_line > used_range.end_line) then
            return false -- Overlaps with used range
        end
    end

    return true
end

-- Mark a range as used
---@param start_line integer Start of used range
---@param end_line integer End of used range
function SearchEngine:_mark_range_used(start_line, end_line)
    ---@type UsedRange
    local new_range = {
        start_line = start_line,
        end_line = end_line,
    }
    table.insert(self.used_ranges, new_range)
end

-- Evaluate a specific position for match quality
---@param search_lines string[] Lines to search for
---@param file_lines string[] All file lines
---@param start_pos integer Starting position to evaluate
---@return SearchResult result Match evaluation result
function SearchEngine:_evaluate_position(search_lines, file_lines, start_pos)
    local search_len = #search_lines

    -- Bounds check
    if start_pos + search_len - 1 > #file_lines then
        return {
            score = 0,
            start_line = start_pos,
            line_details = {},
            match_type = types.OVERALL_MATCH_TYPE.fuzzy_low,
        }
    end

    ---@type LineMatchDetail[]
    local line_details = {}
    local total_score = 0
    local all_exact = true
    local all_whitespace_or_better = true

    -- Analyze each line
    for i = 1, search_len do
        local search_line = search_lines[i]
        local file_line = file_lines[start_pos + i - 1]
        local absolute_line_num = start_pos + i - 1

        local match_type, line_score, differences = string_utils.compare_lines(search_line, file_line)
        total_score = total_score + line_score

        ---@type LineMatchDetail
        local line_detail = {
            line_number = absolute_line_num,
            expected_line = search_line,
            found_line = file_line,
            line_score = line_score,
            line_match_type = match_type,
            differences = differences or {},
        }
        table.insert(line_details, line_detail)

        -- Track overall match quality (early exit for performance)
        if match_type ~= types.LINE_MATCH_TYPE.exact then
            all_exact = false
        end
        if match_type ~= types.LINE_MATCH_TYPE.exact and match_type ~= types.LINE_MATCH_TYPE.exact_whitespace then
            all_whitespace_or_better = false
        end
    end

    local avg_score = total_score / search_len

    -- Determine overall match type with clear thresholds
    local overall_match_type
    if all_exact then
        overall_match_type = types.OVERALL_MATCH_TYPE.exact
    elseif all_whitespace_or_better then
        overall_match_type = types.OVERALL_MATCH_TYPE.exact_whitespace
    elseif avg_score >= 0.85 then
        overall_match_type = types.OVERALL_MATCH_TYPE.fuzzy_high
    elseif avg_score >= 0.70 then
        overall_match_type = types.OVERALL_MATCH_TYPE.fuzzy_medium
    else
        overall_match_type = types.OVERALL_MATCH_TYPE.fuzzy_low
    end

    return {
        score = avg_score,
        start_line = start_pos,
        line_details = line_details,
        match_type = overall_match_type,
    }
end

-- Check if one result is better than another
---@param new_result SearchResult New result to compare
---@param current_best SearchResult Current best result
---@return boolean is_better
function SearchEngine:_is_better_result(new_result, current_best)
    -- Define priority order for match types
    local type_priority = {
        [types.OVERALL_MATCH_TYPE.exact] = 4,
        [types.OVERALL_MATCH_TYPE.exact_whitespace] = 3,
        [types.OVERALL_MATCH_TYPE.fuzzy_high] = 2,
        [types.OVERALL_MATCH_TYPE.fuzzy_medium] = 1,
        [types.OVERALL_MATCH_TYPE.fuzzy_low] = 0,
    }

    local new_priority = type_priority[new_result.match_type] or 0
    local current_priority = type_priority[current_best.match_type] or 0

    -- First compare by match type priority
    if new_priority ~= current_priority then
        return new_priority > current_priority
    end

    -- If same priority, compare by score
    return new_result.score >= current_best.score
end

-- Create successful search result
---@param best_result table Best match found
---@param search_lines string[] Original search lines
---@param file_lines string[] All file lines
---@return BlockLocationResult
function SearchEngine:_create_success_result(best_result, search_lines, file_lines)
    local end_line = best_result.start_line + #search_lines - 1
    local found_lines = vim.list_slice(file_lines, best_result.start_line, end_line)

    return {
        found = true,
        start_line = best_result.start_line,
        end_line = end_line,
        overall_score = best_result.score,
        overall_match_type = best_result.match_type,
        confidence = math.floor(best_result.score * 100),
        found_content = table.concat(found_lines, "\n"),
        found_lines = found_lines,
        line_details = best_result.line_details,
    }
end

return SearchEngine

--[[
--NOTE: Having cmd = "MCPHub" or lazy = true in user's lazy config, and adding lualine component using require("mcphub.extensions.lualine") will start the hub indirectly.
--]]

-- DEPRECATED: This lualine component will load MCPHub even with lazy loading.
-- Use the global variables approach instead: vim.g.mcphub_status, vim.g.mcphub_servers_count, vim.g.mcphub_executing
-- See documentation for recommended usage: doc/extensions/lualine.md

local M = require("lualine.component"):extend()
local utils = require("lualine.utils.utils")
local spinner_frames = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
local spinner_interval = 80 -- ms between frames
local timer = nil
local current_frame = 1

local default_options = {
    icon = "󰐻",
    colored = true,
    colors = {
        connected = { fg = utils.extract_highlight_colors("DiagnosticInfo", "fg") },
        connecting = { fg = utils.extract_highlight_colors("DiagnosticWarn", "fg") },
        error = { fg = utils.extract_highlight_colors("DiagnosticError", "fg") },
    },
}

M.HubState = {
    STARTING = "starting",
    READY = "ready",
    ERROR = "error",
    RESTARTING = "restarting",
    RESTARTED = "restarted",
    STOPPED = "stopped",
    STOPPING = "stopping",
}

vim.g.mcphub_status = M.HubState.STARTING
-- Initialize the component
function M:init(options)
    vim.notify_once(
        "MCPHub lualine extension is deprecated. Use global variables instead for better lazy-loading. See :help mcphub-lualine",
        vim.log.levels.WARN
    )
    M.super.init(self, options)
    self:create_autocommands()
    self.options = vim.tbl_deep_extend("keep", self.options or {}, default_options)
    self.highlights = {
        error = self:create_hl(self.options.colors.error, "error"),
        connecting = self:create_hl(self.options.colors.connecting, "connecting"),
        connected = self:create_hl(self.options.colors.connected, "connected"),
    }
    -- Determine whether the plugin's own coloring logic should apply to the icon or status text.
    self.plugin_colors_icon = self.options.colored and not self.options.icon_color_highlight
    self.plugin_colors_status = self.options.colored and not self.options.color_highlight
end

function M:create_autocommands()
    local group = vim.api.nvim_create_augroup("mcphub_lualine", { clear = true })

    -- Handle state changes
    vim.api.nvim_create_autocmd("User", {
        group = group,
        pattern = "MCPHubStateChange",
        callback = function(args)
            self:manage_spinner()
            if args.data then
                vim.g.mcphub_status = args.data.state
                vim.g.mcphub_active_servers = args.data.active_servers
            end
        end,
    })

    -- Tool/Resource activity events
    vim.api.nvim_create_autocmd("User", {
        group = group,
        pattern = { "MCPHub*" },
        callback = function(args)
            -- Update based on event pattern
            if args.match == "MCPHubToolStart" then
                vim.g.mcphub_executing = true
                vim.g.mcphub_tool_active = true
                vim.g.mcphub_tool_info = args.data
            elseif args.match == "MCPHubToolEnd" then
                vim.g.mcphub_executing = false
                vim.g.mcphub_tool_active = false
                vim.g.mcphub_tool_info = nil
            elseif args.match == "MCPHubResourceStart" then
                vim.g.mcphub_executing = true
                vim.g.mcphub_resource_active = true
                vim.g.mcphub_resource_info = args.data
            elseif args.match == "MCPHubResourceEnd" then
                vim.g.mcphub_executing = false
                vim.g.mcphub_resource_active = false
                vim.g.mcphub_resource_info = nil
            elseif args.match == "MCPHubPromptStart" then
                vim.g.mcphub_executing = true
                vim.g.mcphub_prompt_active = true
                vim.g.mcphub_prompt_info = args.data
            elseif args.match == "MCPHubPromptEnd" then
                vim.g.mcphub_executing = false
                vim.g.mcphub_prompt_active = false
                vim.g.mcphub_prompt_info = nil
            elseif args.match == "MCPHubServersUpdated" then
                if args.data then
                    vim.g.mcphub_active_servers = args.data.active_servers
                end
            end
            -- Manage animation
            self:manage_spinner()
        end,
    })
end

function M.is_connected()
    return vim.g.mcphub_status == M.HubState.READY or vim.g.mcphub_status == M.HubState.RESTARTED
end

function M.is_connecting()
    return vim.g.mcphub_status == M.HubState.STARTING or vim.g.mcphub_status == M.HubState.RESTARTING
end

function M:manage_spinner()
    local should_show = vim.g.mcphub_executing and M.is_connected()
    if should_show and not timer then
        timer = vim.loop.new_timer()
        if timer then
            timer:start(
                0,
                spinner_interval,
                vim.schedule_wrap(function()
                    current_frame = (current_frame % #spinner_frames) + 1
                    vim.cmd("redrawstatus")
                end)
            )
        end
    elseif not should_show and timer then
        timer:stop()
        timer:close()
        timer = nil
        current_frame = 1
    end
end

-- Get appropriate status icon and highlight
function M:get_status_display()
    local tower = "󰐻"
    return tower, M.is_connected() and "DiagnosticInfo" or M.is_connecting() and "DiagnosticWarn" or "DiagnosticError"
end

-- Update function that lualine calls
function M:update_status()
    -- Show either the spinner or the number of active servers
    local count_or_spinner = vim.g.mcphub_executing and spinner_frames[current_frame]
        or tostring(vim.g.mcphub_active_servers or 0)

    -- Set the group highlight appropriately.
    local highlight = M.is_connected() and self.highlights.connected
        or M.is_connecting() and self.highlights.connecting
        or self.highlights.error

    if self.plugin_colors_icon then
        self.options.icon_color_highlight = highlight
    end
    if self.plugin_colors_status then
        self.options.color_highlight = highlight
    end

    return count_or_spinner
end

-- Cleanup
function M:disable()
    if timer then
        timer:stop()
        timer:close()
        timer = nil
    end
end

return M

-- EditSession - Main orchestrator for the editing workflow
-- Coordinates parsing, location, and UI components

---@class EditSession
---@field origin_winnr number Window number where the session started
---@field file_path string Path to the file being edited
---@field diff_content string Raw diff content from LLM
---@field config EditSessionConfig Configuration options
---@field parser DiffParser Parser instance
---@field locator BlockLocator Block locator instance
---@field located_blocks LocatedBlock[] Located blocks after parsing and locating
---@field ui EditUI? UI instance (created when needed)
---@field callbacks table Success/error callbacks
local EditSession = {}
EditSession.__index = EditSession

local Path = require("plenary.path")
local DEFAULT_CONFIG = {
    parser = {
        track_issues = true,
        extract_inline_content = true,
    },
    locator = {
        fuzzy_threshold = 0.8,
        enable_fuzzy_matching = true,
    },
    ui = {
        go_to_origin_on_complete = true,
        keybindings = {
            accept = ".", -- Accept current change
            reject = ",", -- Reject current change
            next = "n", -- Next diff
            prev = "p", -- Previous diff
            accept_all = "ga", -- Accept all remaining changes
            reject_all = "gr", -- Reject all remaining changes
        },
    },
    feedback = {
        include_parser_feedback = true,
        include_locator_feedback = true,
        include_ui_summary = true,
        ui = {
            include_session_summary = true,
            include_final_diff = true,
            send_diagnostics = true,
            wait_for_diagnostics = 500,
            diagnostic_severity = vim.diagnostic.severity.WARN, -- Only show warnings and above by default
        },
    },
}

-- Create new edit session
---@param file_path string Path to file to edit
---@param diff_content string Raw diff content
---@param config EditSessionConfig? Optional configuration
---@return EditSession
function EditSession.new(file_path, diff_content, config)
    local self = setmetatable({}, EditSession)

    self.file_path = file_path
    self.diff_content = diff_content
    self.config = vim.tbl_deep_extend("force", DEFAULT_CONFIG, config or {})

    self.origin_winnr = vim.api.nvim_get_current_win()
    -- Load components
    local DiffParser = require("mcphub.native.neovim.files.edit_file.diff_parser")
    local BlockLocator = require("mcphub.native.neovim.files.edit_file.block_locator")

    self.parser = DiffParser.new(self.config.parser)
    self.locator = BlockLocator.new(self.config.locator)

    return self
end

--- Get the content of the buffer if available or read from file
---@param path string Path to the file
---@param bufnr number? Buffer number to read from (if available)
---@return string content File content or nil if not found
function EditSession:get_file_content(path, bufnr)
    --INFO: unloaded buffers return empty lines
    if bufnr and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr) then
        return table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n")
    end

    local file_path = Path:new(path)
    if file_path:exists() and file_path:is_file() then
        return file_path:read()
    end
    return ""
end

--- Start the editing session
---@param options table Options including on_success, on_error callbacks
function EditSession:start(options)
    self.callbacks = {
        on_success = options.on_success or function() end,
        on_error = options.on_error or function() end,
    }
    local buf_utils = require("mcphub.native.neovim.utils.buffer")
    local buf_info = buf_utils.find_buffer(self.file_path) or {}
    local file_content = self:get_file_content(self.file_path, buf_info.bufnr)
    local is_replacing_entire_file = options.replace_file_content ~= nil

    ---@type ParsedBlock[]
    local parsed_blocks = {}
    --- Needed to avoid parsing error when the file content has markers
    if is_replacing_entire_file then
        table.insert(parsed_blocks, {
            block_id = "Block 1",
            search_content = "",
            search_lines = {},
            replace_content = options.replace_file_content,
            replace_lines = vim.split(options.replace_file_content, "\n", { plain = true, trimempty = true }),
        }--[[@as ParsedBlock]])
    else
        local _parsed_blocks, parse_error = self.parser:parse(self.diff_content)
        if not _parsed_blocks then
            return self:_handle_error("Failed to parse diff: " .. parse_error)
        end
        parsed_blocks = _parsed_blocks
    end

    for _, block in ipairs(parsed_blocks) do
        if vim.trim(block.search_content) == "" then
            if #parsed_blocks > 1 then
                return self:_handle_error(
                    string.format(
                        "A Block with empty search content found, but multiple blocks are present in the diff. %s will replace the entire file. If you want to write the entire file, please use a single SEARCH/REPLACE block with empty SEARCH content. If you want to replace a section of the file, please provide non-whitespace SEARCH content.",
                        block.block_id
                    )
                )
            end
            is_replacing_entire_file = true
            break
        else
            -- If we are searching for something in a file that doesn't exist, we should not proceed
            if file_content == "" then
                return self:_handle_error(
                    string.format(
                        "Editing `%s` failed. The file does not exist. If you are using relative paths make sure the path is relative to the cwd or use an absolute path.",
                        self.file_path
                    )
                )
            end
        end
    end

    local located_blocks = self.locator:locate_all_blocks(parsed_blocks, file_content)
    self.located_blocks = located_blocks

    local failed_blocks = vim.tbl_filter(function(block)
        return not block.location_result.found
    end, located_blocks)
    if #failed_blocks > 0 then
        return self:_handle_error(
            string.format(
                "## Editing `%s` failed. No changes were made to the file. Couldn't find %d of %d block(s). Please see the <BESTMATCH/> content provided in the SEARCHING feedback.",
                self.file_path,
                #failed_blocks,
                #located_blocks
            )
        )
    end

    local EditUI = require("mcphub.native.neovim.files.edit_file.edit_ui")
    self.ui = EditUI.new(self.config.ui)

    self.ui:start_interactive_editing({
        interactive = options.interactive ~= false,
        is_replacing_entire_file = is_replacing_entire_file,
        origin_winnr = self.origin_winnr,
        file_path = self.file_path,
        located_blocks = self.located_blocks,
        original_content = file_content,
        on_complete = function()
            self:_generate_final_report(false, function(report)
                self.callbacks.on_success("# EDIT SESSION\n\n" .. report)
                self.ui:cleanup()
            end)
        end,
        on_cancel = function(reason)
            self:_generate_final_report(true, function(feedback_report)
                local final_report = string.format(
                    "# EDIT SESSION\n\n%s%s",
                    reason or "User rejected the changes to the file",
                    feedback_report ~= "" and ("\n\n" .. feedback_report) or ""
                )
                self.callbacks.on_error(final_report)
                self.ui:cleanup()
            end)
        end,
    })
end

-- Handle errors with comprehensive feedback
---@param error_msg string Error message
function EditSession:_handle_error(error_msg)
    self:_generate_final_report(true, function(feedback_report)
        local final_report = string.format(
            "# EDIT SESSION\n\n%s%s",
            error_msg or "User rejected the changes to the file",
            feedback_report ~= "" and ("\n\n" .. feedback_report) or ""
        )
        self.callbacks.on_error(final_report)
    end)
end

-- Generate final report by orchestrating feedback from all components
---@param is_cancelled boolean Whether the session was cancelled
---@param on_report_ready function Callback to receive the final report
function EditSession:_generate_final_report(is_cancelled, on_report_ready)
    local feedback_parts = {}
    local feedback_config = self.config.feedback

    -- Get Parser Feedback
    if feedback_config.include_parser_feedback then
        local parser_feedback = self.parser:get_feedback()
        if parser_feedback then
            table.insert(feedback_parts, parser_feedback)
        end
    end

    -- Get Locator Feedback
    if feedback_config.include_locator_feedback then
        local locator_feedback = self.locator:get_feedback(self.located_blocks)
        if locator_feedback then
            table.insert(feedback_parts, locator_feedback)
        end
    end

    -- Get UI Summary (Asynchronous due to waiting for diagnostics)
    if feedback_config.include_ui_summary and self.ui and not is_cancelled then
        self.ui:get_summary(feedback_config.ui, function(ui_summary)
            if ui_summary and ui_summary ~= "" then
                table.insert(feedback_parts, ui_summary)
            end
            on_report_ready(table.concat(feedback_parts, "\n\n"))
        end)
    else
        on_report_ready(table.concat(feedback_parts, "\n\n"))
    end
end

return EditSession

local mcphub = require("mcphub")
local prompt_utils = require("mcphub.utils.prompt")

mcphub.add_tool("mcphub", {
    name = "get_current_servers",
    description = "Get the current state of all MCP servers (connected and disabled). This is useful when you need to know what servers are currently available, especially when restoring chat from history or when server state may have changed.",
    inputSchema = {
        type = "object",
        properties = {
            include_disabled = {
                type = "boolean",
                description = "Whether to include disabled servers in the response (default: true)",
                default = true,
            },
            format = {
                type = "string",
                description = "Response format: 'detailed' for full server info or 'summary' for compact list (default: detailed)",
                enum = { "detailed", "summary" },
                default = "detailed",
            },
        },
    },
    handler = function(req, res)
        local hub = mcphub.get_hub_instance()
        if not hub or not hub:is_ready() then
            return res:error("Hub is not ready")
        end

        local include_disabled = req.params.include_disabled ~= false -- default to true
        local format = req.params.format or "detailed"

        if format == "summary" then
            local connected_count = 0
            local disabled_count = 0
            local server_names = { connected = {}, disabled = {} }

            local servers = hub:get_servers(include_disabled)
            for _, server in ipairs(servers) do
                if server.status == "connected" then
                    connected_count = connected_count + 1
                    table.insert(server_names.connected, server.name)
                elseif server.status == "disabled" then
                    disabled_count = disabled_count + 1
                    table.insert(server_names.disabled, server.name)
                end
            end

            local summary = string.format(
                "# Current MCP Server Status\n\n" .. "Connected servers (%d): %s\n" .. "Disabled servers (%d): %s",
                connected_count,
                #server_names.connected > 0 and table.concat(server_names.connected, ", ") or "None",
                disabled_count,
                #server_names.disabled > 0 and table.concat(server_names.disabled, ", ") or "None"
            )

            return res:text(summary):send()
        else
            -- Detailed format using the existing active servers prompt
            local detailed_info = hub:get_active_servers_prompt(true, include_disabled)
            return res:text(detailed_info):send()
        end
    end,
})

mcphub.add_tool("mcphub", {
    name = "toggle_mcp_server",
    description = "Start or stop an MCP server. You can only start a server from one of the disabled servers.",
    inputSchema = {
        type = "object",
        properties = {
            server_name = {
                type = "string",
                description = "Name of the MCP server to toggle",
            },
            action = {
                type = "string",
                description = "Action to perform. One of 'start' or 'stop'",
                enum = { "start", "stop" },
            },
        },
        required = { "server_name", "action" },
    },
    handler = function(req, res)
        local hub = mcphub.get_hub_instance()
        if not hub or not hub:is_ready() then
            return res:error("Hub is not ready")
        end

        local server_name = req.params.server_name
        local action = req.params.action
        if not server_name or not action then
            return res:error("Missing required parameters: server_name and action")
        end

        -- Check if server exists in current state
        local found = false
        for _, server in ipairs(hub:get_servers(true)) do
            if server.name == server_name then
                found = true
                break
            end
        end

        if not found then
            return res:error(string.format("Server '%s' not found in active servers", server_name))
        end

        --INFO: via_curl_request: because we can wait for the server to start or stop and send the correct status to llm rather than sse event based on file wathing which is more appropriate for UI
        if action == "start" then
            hub:start_mcp_server(server_name, {
                via_curl_request = true,
                callback = function(response, err)
                    if err then
                        return res:error(string.format("Failed to start MCP server: %s", err))
                    end
                    local server = response and response.server
                    return res
                        :text(
                            string.format("Started MCP server: %s\n%s", server_name, hub:convert_server_to_text(server))
                        )
                        :send()
                end,
            })
        elseif action == "stop" then
            hub:stop_mcp_server(server_name, true, {
                via_curl_request = true,
                callback = function(_, err)
                    if err then
                        return res:error(string.format("Failed to stop MCP server: %s", err))
                    end
                    return res:text(string.format("Stopped MCP server: %s.", server_name)):send()
                end,
            })
        else
            return res:error(string.format("Invalid action '%s'. Use 'start' or 'stop'", action))
        end
    end,
})

mcphub.add_resource("mcphub", {
    name = "MCPHub Plugin Docs",
    mimeType = "text/plain",
    uri = "mcphub://docs",
    description = [[Documentation for the mcphub.nvim plugin for Neovim.]],
    handler = function(_, res)
        local guide = prompt_utils.get_plugin_docs()
        if not guide then
            return res:error("Plugin docs not available")
        end
        return res:text(guide):send()
    end,
})

mcphub.add_resource("mcphub", {
    name = "MCPHub Native Server Guide",
    mimeType = "text/plain",
    uri = "mcphub://native_server_guide",
    description = [[Documentation on how to create Lua Native MCP servers for mcphub.nvim plugin.
This guide is intended for Large language models to help users create their own native servers for mcphub.nvim plugin.
Access this guide whenever you need information on how to create a native server for mcphub.nvim plugin.]],
    handler = function(_, res)
        local guide = prompt_utils.get_native_server_prompt()
        if not guide then
            return res:error("Native server guide not available")
        end
        return res:text(guide):send()
    end,
})

mcphub.add_resource("mcphub", {
    name = "MCPHub Changelog",
    mimeType = "text/plain",
    uri = "mcphub://changelog",
    description = [[Changelog for the mcphub.nvim plugin for Neovim.]],
    handler = function(_, res)
        local guide = prompt_utils.get_plugin_changelog()
        if not guide then
            return res:error("Plugin changelog not available")
        end
        return res:text(guide):send()
    end,
})

local Request = require("mcphub.native.utils.request")
local Response = require("mcphub.native.utils.response")
local State = require("mcphub.state")
local buf_utils = require("mcphub.native.neovim.utils.buffer")
local log = require("mcphub.utils.log")

---@class MCPTool
---@field name string Tool identifier
---@field description string|fun():string Tool description or function returning description
---@field inputSchema? table|fun():table JSON Schema for input validation or function returning schema
---@field handler fun(req: ToolRequest, res: ToolResponse): nil | table Tool handler function
---@field needs_confirmation_window? boolean Whether the tool requires a confirmation window before execution

---@class MCPResource
---@field name? string Resource identifier
---@field description? string|fun():string Resource description or function returning description
---@field mimeType? string Resource MIME type (e.g., "text/plain")
---@field uri string Static URI (e.g., "system://info")
---@field handler fun(req: ResourceRequest, res: ResourceResponse): nil | table Resource handler function

---@class MCPPromptArgument
---@field name string Argument name
---@field description? string Argument description
---@field required? boolean Whether the argument is required
---@field default? string Default value for the argument

---@class MCPPrompt
---@field name? string Prompt identifier
---@field description? string|fun():string Prompt description or function returning description
---@field arguments? MCPPromptArgument[]|fun():MCPPromptArgument[] List of arguments
---@field handler? fun(req: PromptRequest, res: PromptResponse): nil | table

---@class MCPResourceTemplate
---@field name? string Template identifier
---@field description? string|fun():string Template description or function returning description
---@field mimeType? string Template MIME type (e.g., "text/plain")
---@field uriTemplate string URI with parameters (e.g., "buffer://{bufnr}/lines")
---@field handler fun(req: ResourceRequest, res: ResourceResponse): nil | table Template handler function

---@class MCPCapabilities
---@field tools? MCPTool[] List of tools
---@field resources? MCPResource[] List of resources
---@field resourceTemplates? MCPResourceTemplate[] List of resource templates
---@field prompts? MCPPrompt[] List of prompts

---@class NativeServer
---@field name string Server name
---@field displayName? string Display name
---@field description? string Server description
---@field status string Server status (connected|disconnected|disabled)
---@field error? string|nil Error message if any
---@field capabilities MCPCapabilities Server capabilities
---@field uptime? number Server uptime
---@field lastStarted? number Last started timestamp
---@field config_source string Include which config file this server came from
---@field is_native boolean Flag to indicate this is a native server
local NativeServer = {}
NativeServer.__index = NativeServer

local TIMEOUT = 5 -- seconds

-- Helper function to extract params from uri using template
-- Note: Parameter values containing slashes (/) or special characters must be URL encoded
-- Example: For a template "file/read/{path}"
--   - "file/read/home%2Fuser%2Ffile.txt" ✓ (correctly encoded)
--   - "file/read/home/user/file.txt"     ✗ (will not match)

local function extract_params(uri, template)
    local params = {}

    -- Convert template into pattern with url-encoded char support
    local pattern = template:gsub("{([^}]+)}", "([^/]+)")

    -- Get param names from template
    local names = {}
    for name in template:gmatch("{([^}]+)}") do
        table.insert(names, name)
    end

    -- Match URI against pattern
    local values = { uri:match("^" .. pattern .. "$") }
    if #values == 0 then
        return nil
    end

    -- Map matched values to param names and decode them
    for i, name in ipairs(names) do
        -- URL decode the parameter value
        local decoded = values[i]:gsub("%%(%x%x)", function(hex)
            return string.char(tonumber(hex, 16))
        end)
        params[name] = decoded
    end

    return params
end

-- Helper function to find matching resource
function NativeServer:find_matching_resource(uri)
    -- Check direct resources first
    for _, resource in ipairs(self.capabilities.resources) do
        if resource.uri == uri then
            log.debug(string.format("Matched uri"))
            return resource, {}
        end
    end

    -- Check templates
    for _, template in ipairs(self.capabilities.resourceTemplates) do
        -- Extract params using template
        log.debug(string.format("Matching uri '%s' against template '%s'", uri, template.uriTemplate))
        local params = extract_params(uri, template.uriTemplate)
        if params then
            log.debug(string.format("Matched uri template with params: %s", vim.inspect(params)))
            return template, params
        end
    end

    return nil
end

--- Create a new native server instance
---@param def table Server definition with name capabilities etc
---@return NativeServer | nil Server instance or nil on error
function NativeServer:new(def)
    -- Validate required fields
    if not def.name then
        log.warn("NativeServer definition must include name")
        return
    end
    if not def.capabilities then
        log.warn("NativeServer definition must include capabilities")
        return
    end

    log.debug({
        code = "NATIVE_SERVER_INIT",
        message = "Creating new native server",
        data = { name = def.name, capabilities = def.capabilities },
    })

    local instance = {
        name = def.name,
        displayName = def.displayName or def.name,
        description = def.description or "",
        status = "connected",
        error = nil,
        capabilities = {
            tools = {},
            resources = {},
            resourceTemplates = {},
            prompts = {},
        },
        uptime = 0,
        lastStarted = os.time(),
        -- Always save native server settings in global config file
        config_source = State.config.config,
        is_native = true, -- Flag to indicate this is a native server
    }
    setmetatable(instance, self)

    -- Initialize capabilities
    instance:initialize(def)

    return instance
end

--- Initialize or reinitialize server capabilities
---@param def table Server definition
function NativeServer:initialize(def)
    -- Reset error state
    self.error = nil
    self.capabilities = {
        tools = def.capabilities.tools or {},
        resources = def.capabilities.resources or {},
        resourceTemplates = def.capabilities.resourceTemplates or {},
        prompts = def.capabilities.prompts or {},
    }

    local config_manager = require("mcphub.utils.config_manager")
    -- Get server config
    local server_config = config_manager.get_server_config(self) or {}
    -- Check if server is disabled
    if server_config.disabled then
        self.status = "disabled"
        return
    end
    self.status = "connected"
    self.lastStarted = os.time()
end

function NativeServer:get_prompt(name, arguments, opts)
    opts = opts or {}
    -- Create output handler
    local got_prompt = false
    local prompt_result, prompt_error
    local function output_handler(result, err)
        if got_prompt then
            return
        end
        prompt_result = result
        prompt_error = err
        got_prompt = true
        if opts.callback then
            opts.callback(result, err)
            return
        end
        return result, err
    end
    log.debug(string.format("Getting prompt '%s' on server '%s'", name, self.name))
    -- Check server state
    if self.status ~= "connected" then
        local err = string.format("Server '%s' is not connected (status: %s)", self.name, self.status)
        log.warn(string.format("Server '%s' is not connected (status: %s)", self.name, self.status))
        return output_handler(nil, err)
    end

    -- Find tool in capabilities
    local prompt
    for _, p in ipairs(self.capabilities.prompts) do
        if p.name == name then
            prompt = p
            break
        end
    end
    if not prompt then
        local err = string.format("Prompt '%s' not found", name)
        log.warn(string.format("Prompt '%s' not found", name))
        return output_handler(nil, err)
    end

    local editor_info = buf_utils.get_editor_info()
    -- Create req/res objects with full context
    local req = Request.PromptRequest:new({
        server = self,
        prompt = prompt,
        arguments = arguments,
        caller = opts.caller,
        editor_info = editor_info,
    })
    local res = Response.PromptResponse:new(output_handler, prompt.name, prompt.description)

    -- Execute tool with req/res
    local ok, result = pcall(prompt.handler, req, res)
    if not ok then
        log.warn(string.format("Tool execution failed: %s", result))
        return res:error(result)
    end

    -- Handle synchronous return if any
    if result ~= nil then
        return output_handler(result)
    end
    -- If native_server:get_prompt is a synchronous call but the handler didn't return anything or is running asynchronously given the res:send() arch
    -- Wait for the handler to finish until TIMEOUT as if the user didn't call res:send() this will never finish
    -- The only place the nativeserver is called synchronously is while a chat resolving  prompt
    local start_time = os.time()
    if not opts.callback then
        while not got_prompt do
            vim.wait(500)
            if os.time() - start_time > TIMEOUT then
                return output_handler(nil, "Prompt access timed out")
            end
        end
        return prompt_result, prompt_error
    end
end

--- Execute a tool by name
---@param name string Tool name to execute
---@param arguments table Arguments for the tool
---@return table|nil result Tool execution result
---@return string|nil error Error message if any
function NativeServer:call_tool(name, arguments, opts)
    opts = opts or {}
    -- Create output handler
    -- Track if tool has completed to prevent double-handling
    local tool_finished = false
    local tool_result, tool_error
    local function output_handler(result, err)
        if tool_finished then
            return
        end
        tool_result = result
        tool_error = err
        tool_finished = true
        if opts.callback then
            opts.callback(result, err)
            return
        end
        return result, err
    end
    log.debug(string.format("Calling tool '%s' on server '%s'", name, self.name))
    -- Check server state
    if self.status ~= "connected" then
        local err = string.format("Server '%s' is not connected (status: %s)", self.name, self.status)
        log.warn(string.format("Server '%s' is not connected (status: %s)", self.name, self.status))
        return output_handler(nil, err)
    end

    -- Find tool in capabilities
    local tool
    for _, t in ipairs(self.capabilities.tools) do
        if t.name == name then
            tool = t
            break
        end
    end
    if not tool then
        local err = string.format("Tool '%s' not found", name)
        log.warn(string.format("Tool '%s' not found", name))
        return output_handler(nil, err)
    end

    local editor_info = buf_utils.get_editor_info()
    -- Create req/res objects with full context
    local req = Request.ToolRequest:new({
        server = self,
        tool = tool,
        arguments = arguments,
        caller = opts.caller,
        editor_info = editor_info,
    })
    local res = Response.ToolResponse:new(output_handler)

    -- Execute tool with req/res
    local ok, result = pcall(tool.handler, req, res)
    if not ok then
        log.warn(string.format("Tool execution failed: %s", result))
        return res:error(result)
    end

    -- Handle synchronous return if any
    if result ~= nil then
        return output_handler(result)
    end
    -- -- If native_server:call_tool is a synchronous call but the handler didn't return anything or is running asynchronously given the res:send() arch
    -- -- Wait for the handler to finish until TIMEOUT as if the user didn't call res:send() this will never finish
    -- -- The only place the nativeserver is called synchronously is while a chat resolving #resource variable in the chat when submitted
    local start_time = os.time()
    if not opts.callback then
        while not tool_finished do
            vim.wait(500)
            if os.time() - start_time > TIMEOUT then
                return output_handler(nil, "Tool execution timed out")
            end
        end
        return tool_result, tool_error
    end
end

function NativeServer:access_resource(uri, opts)
    opts = opts or {}
    -- Create output handler
    -- Track if resource has called to prevent double-handling
    local resource_accessed = false
    local resource_result, resource_error
    local function output_handler(result, err)
        if resource_accessed then
            return
        end
        resource_result = result
        resource_error = err
        resource_accessed = true
        if opts.callback then
            opts.callback(result, err)
            return
        end
        return result, err
    end
    -- Check server state
    if self.status ~= "connected" then
        return output_handler(nil, string.format("Server '%s' is not connected (status: %s)", self.name, self.status))
    end

    log.debug(string.format("Accessing resource '%s' on server '%s'", uri, self.name))
    -- Find matching resource/template and extract params
    local resource, params = self:find_matching_resource(uri)
    if not resource then
        local err = string.format("Resource '%s' not found", uri)
        log.warn(string.format("Resource '%s' not found", uri))
        return output_handler(nil, err)
    end

    -- Check if resource has handler
    if not resource.handler then
        local err = "Resource has no handler"
        log.warn(string.format("Resource '%s' has no handler", uri))
        return output_handler(nil, err)
    end

    local editor_info = buf_utils.get_editor_info()
    -- Create req/res objects with full context
    local req = Request.ResourceRequest:new({
        server = self,
        resource = resource,
        uri = uri,
        template = resource.uriTemplate,
        editor_info = editor_info,
        params = params,
        caller = opts.caller,
    })
    local res = Response.ResourceResponse:new(output_handler, uri, resource.uriTemplate)

    -- Call resource handler with req/res
    local ok, result = pcall(resource.handler, req, res)
    if not ok then
        log.warn(string.format("Resource access failed: %s", result))
        return res:error(result --[[@as string]])
    end

    -- Handle synchronous return if any
    if result ~= nil then
        return output_handler(result)
    end
    -- call_resource will be called synchronously when chat is trying to resolve #resourcevariable(that we populated dynamically based on available servers) when chat:submit(),
    -- If native_server:call_resource is a synchronous call but the handler didn't return anything or is running asynchronously given the res:send() arch
    -- Wait for the handler to finish until TIMEOUT as if the user didn't call res:send() this will never finish
    local start_time = os.time()
    if not opts.callback then
        while not resource_accessed do
            vim.wait(500)
            if (os.time() - start_time) > TIMEOUT then
                return output_handler(nil, "Resource access timed out")
            end
        end
        return resource_result, resource_error
    end
end

function NativeServer:start()
    -- Check server state
    if self.status == "connected" then
        return true
    end
    self.status = "connected"
    self.lastStarted = os.time()
    return true
end

function NativeServer:stop(disable)
    disable = disable or false
    -- if disable then
    self.status = "disabled"
    -- else
    --     self.status = "disconnected"
    -- end
end

return NativeServer

local utils = require("mcphub.utils")

local function get_buffer_lines(buf_info)
    local cursor_line = buf_info.cursor_pos[1]
    local ok, lines = pcall(vim.api.nvim_buf_get_lines, buf_info.bufnr, 0, -1, false)
    if not ok or not lines then
        return { "Failed to get buffer lines" }
    end

    local formatted = {}
    for i, line in ipairs(lines) do
        local prefix = i == cursor_line and ">" or " "
        -- Escape any format strings in the line content
        line = line:gsub("%%", "%%%%")
        table.insert(formatted, string.format("%s %4d │ %s", prefix, i, line))
    end
    return formatted
end

local function get_marks(buf_info)
    local marks = vim.fn.getmarklist(buf_info.bufnr)
    local result = {}
    for _, mark in ipairs(marks) do
        if mark.mark:match("[a-zA-Z]$") then
            local ok, line = pcall(vim.api.nvim_buf_get_lines, buf_info.bufnr, mark.pos[2] - 1, mark.pos[2], false)
            local context = ok and line[1] or ""
            if context then
                context = context:gsub("%%", "%%%%")
                table.insert(
                    result,
                    string.format("%s: line %d, col %d: %s", mark.mark:sub(-1), mark.pos[2], mark.pos[3], context)
                )
            end
        end
    end
    return result
end

local function get_qf_entries(buf_info)
    local qf_list = vim.fn.getqflist()
    local result = {}
    for _, item in ipairs(qf_list) do
        if item.bufnr == buf_info.bufnr then
            local ok, line = pcall(vim.api.nvim_buf_get_lines, buf_info.bufnr, item.lnum - 1, item.lnum, false)
            local content = ok and line[1] or ""
            if content then
                content = content:gsub("%%", "%%%%")
                table.insert(result, string.format("%4d │ %s\n     └─ %s", item.lnum, content, item.text))
            end
        end
    end
    return result
end

---@type MCPResource
return {
    name = "Buffer",
    description = "Get detailed information about the currently active buffer including content, cursor position, and buffer metadata",
    uri = "neovim://buffer",
    mimeType = "text/plain",
    handler = function(req, res)
        local buf_info = req.editor_info.last_active
        if not buf_info then
            return res:error("No active buffer found")
        end
        if buf_info.bufnr == 0 then
            return res:error("No valid buffer found")
        end

        local lines = get_buffer_lines(buf_info)
        local marks = get_marks(buf_info)
        local qf_entries = get_qf_entries(buf_info)

        local sep = string.rep("─", 80)

        local text = string.format(
            [[
## Buffer Information
Name: %s
Bufnr: %d
Lines: %d
Cursor: line %d

%s
## Buffer Content
%s%s%s
]],
            buf_info.filename,
            buf_info.bufnr,
            buf_info.line_count,
            buf_info.cursor_pos[1],
            sep,
            table.concat(lines, "\n"),
            #marks > 0 and string.format("\n\n%s\n# Marks\n%s", sep, table.concat(marks, "\n")) or "",
            #qf_entries > 0 and string.format("\n\n%s\n# Quickfix Entries\n%s", sep, table.concat(qf_entries, "\n"))
                or ""
        )

        return res:text(text):send()
    end,
}

local M = {}

---@class FileInfo
---@field name string # File name or path
---@field type string # File type (file/directory/etc)
---@field size number # File size in bytes
---@field modified number # Last modification timestamp

---@class DirectoryInfo
---@field path string # Current directory path
---@field is_git boolean # Whether the directory is a git repository
---@field files FileInfo[] # List of files in the directory

---@param path? string # Directory path to scan (defaults to current working directory)
---@return DirectoryInfo
function M.get_directory_info(path)
    path = path or vim.loop.cwd()
    -- Check if git repo
    local is_git = vim.fn.system("git rev-parse --is-inside-work-tree 2>/dev/null"):match("true")
    local files = {}

    if is_git then
        -- Use git ls-files for git-aware listing
        local git_files = vim.fn.systemlist("git ls-files --cached --others --exclude-standard")
        for _, file in ipairs(git_files) do
            local stat = vim.loop.fs_stat(file)
            if stat then
                table.insert(files, {
                    name = file,
                    type = stat.type,
                    size = stat.size,
                    modified = stat.mtime.sec,
                })
            end
        end
    else
        -- Fallback to regular directory listing
        local handle = vim.loop.fs_scandir(path)
        if handle then
            while true do
                local name, type = vim.loop.fs_scandir_next(handle)
                if not name then
                    break
                end

                local stat = vim.loop.fs_stat(name)
                if stat then
                    table.insert(files, {
                        name = name,
                        type = type or stat.type,
                        size = stat.size,
                        modified = stat.mtime.sec,
                    })
                end
            end
        end
    end

    return {
        path = path,
        is_git = is_git,
        files = files,
    }
end
---@class BufferInfo
---@field name string
---@field filename string
---@field windows number[]
---@field winnr number
---@field cursor_pos number[]
---@field filetype string
---@field line_count number
---@field is_visible boolean
---@field is_modified boolean
---@field is_loaded boolean
---@field lastused number
---@field bufnr number

---@class EditorInfo
---@field last_active BufferInfo
---@field buffers BufferInfo[]

---@return EditorInfo
function M.get_editor_info()
    local buffers = vim.fn.getbufinfo({ buflisted = 1 })
    local valid_buffers = {}
    local last_active = nil
    local max_lastused = 0

    for _, buf in ipairs(buffers) do
        -- Only include valid files (non-empty name and empty buftype)
        local buftype = vim.api.nvim_buf_get_option(buf.bufnr, "buftype")
        if buf.name ~= "" and buftype == "" then
            local buffer_info = {
                bufnr = buf.bufnr,
                name = buf.name,
                filename = buf.name,
                is_visible = #buf.windows > 0,
                is_modified = buf.changed == 1,
                is_loaded = buf.loaded == 1,
                lastused = buf.lastused,
                windows = buf.windows,
                winnr = buf.windows[1], -- Primary window showing this buffer
            }

            -- Add cursor info for currently visible buffers
            if buffer_info.is_visible then
                local win = buffer_info.winnr
                local cursor = vim.api.nvim_win_get_cursor(win)
                buffer_info.cursor_pos = cursor
            end

            -- Add additional buffer info
            buffer_info.filetype = vim.api.nvim_buf_get_option(buf.bufnr, "filetype")
            buffer_info.line_count = vim.api.nvim_buf_line_count(buf.bufnr)

            table.insert(valid_buffers, buffer_info)

            -- Track the most recently used buffer
            if buf.lastused > max_lastused then
                max_lastused = buf.lastused
                last_active = buffer_info
            end
        end
    end

    -- If no valid buffers found, provide default last_active
    if not last_active and #valid_buffers > 0 then
        last_active = valid_buffers[1]
    end

    return {
        last_active = last_active,
        buffers = valid_buffers,
    }
end

---@param file_path string The path to the file to find
---@return BufferInfo|nil The buffer info if found, nil otherwise
function M.find_buffer(file_path)
    local bufs = M.get_editor_info().buffers
    for _, buf in ipairs(bufs) do
        if buf.filename == file_path then
            return buf
        end
    end
    return nil
end

---Open file in editor and get target window
---@param file_path string The path to the file to open
---@return number|nil The buffer number of the opened file, or nil if not found
function M.open_file_in_editor(file_path)
    local abs_path = vim.fn.fnamemodify(file_path, ":p")

    local function safe_edit(file)
        local ok = pcall(vim.cmd.edit, vim.fn.fnameescape(file))
        if not ok then
            vim.cmd.enew()
            vim.cmd.file(file)
        end
    end

    -- Try to find existing window with the file
    for _, winid in ipairs(vim.api.nvim_list_wins()) do
        local bufnr = vim.api.nvim_win_get_buf(winid)
        local buf_name = vim.api.nvim_buf_get_name(bufnr)
        if buf_name == abs_path then
            vim.api.nvim_set_current_win(winid)
            return vim.api.nvim_get_current_buf()
        end
    end

    local editor_info = M.get_editor_info()
    -- Try to use last active buffer's window if available
    if editor_info and editor_info.last_active then
        local last_active = editor_info.last_active
        if last_active.winnr and vim.api.nvim_win_is_valid(last_active.winnr) then
            vim.api.nvim_set_current_win(last_active.winnr)
            safe_edit(abs_path)
            return vim.api.nvim_get_current_buf()
        end
    end

    -- Create new window for the file
    local chat_win = vim.api.nvim_get_current_win()
    local chat_col = vim.api.nvim_win_get_position(chat_win)[2]
    local total_width = vim.o.columns

    -- Determine where to place new window based on chat position
    if chat_col > total_width / 2 then
        vim.cmd("topleft vnew") -- New window on the left
    else
        vim.cmd("botright vnew") -- New window on the right
    end

    safe_edit(abs_path)
    local bufnr = vim.api.nvim_get_current_buf()
    vim.api.nvim_buf_set_option(bufnr, "buflisted", true)

    return bufnr
end
return M

local api = vim.api
local Path = require("plenary.path")
local mcphub = require("mcphub")

-- Tool to execute Lua code using nvim_exec2
mcphub.add_tool("neovim", {
    name = "execute_lua",
    description = [[Execute Lua code in Neovim using nvim_exec2 with lua heredoc.]],
    inputSchema = {
        type = "object",
        properties = {
            code = {
                type = "string",
                description = [[
Lua code to execute:

String Formatting Guide:
1. Newlines in Code:
   - Use \n for new lines in your code
   - Example: "local x = 1\nprint(x)"

2. Newlines in Output:
   - Use \\n when you want to print newlines
   - Example: print('Line 1\\nLine 2')

3. Complex Data:
   - Use vim.print() for formatted output
   - Use vim.inspect() for complex structures
   - Both handle escaping automatically

4. String Concatenation:
   - Prefer '..' over string.format()
   - Example: print('Count: ' .. vim.api.nvim_buf_line_count(0))
          ]],
                examples = {
                    -- Simple multiline code
                    "local bufnr = vim.api.nvim_get_current_buf()\nprint('Current buffer:', bufnr)",

                    -- Output with newlines
                    "print('Buffer Info:\\nNumber: ' .. vim.api.nvim_get_current_buf())",

                    -- Complex info with proper formatting
                    [[local bufnr = vim.api.nvim_get_current_buf()
local name = vim.api.nvim_buf_get_name(bufnr)
local ft = vim.bo[bufnr].filetype
local lines = vim.api.nvim_buf_line_count(bufnr)
print('Buffer Info:\\nBuffer Number: ' .. bufnr .. '\\nFile Name: ' .. name .. '\\nFiletype: ' .. ft .. '\\nTotal Lines: ' .. lines)]],

                    -- Using vim.print for complex data
                    [[local info = {
  buffer = vim.api.nvim_get_current_buf(),
  name = vim.api.nvim_buf_get_name(0),
  lines = vim.api.nvim_buf_line_count(0)
}
vim.print(info)]],
                },
            },
        },
        required = { "code" },
    },
    handler = function(req, res)
        local code = req.params.code
        if not code then
            return res:error("code field is required."):send()
        end

        -- Construct Lua heredoc
        local src = string.format(
            [[
lua << EOF
%s
EOF]],
            code
        )

        -- Execute with output capture
        local result = api.nvim_exec2(src, { output = true })

        if result.output then
            return res:text(result.output):send()
        else
            return res:text("Code executed successfully. (No output)"):send()
        end
    end,
})

-- Tool to execute shell commands using jobstart
mcphub.add_tool("neovim", {
    name = "execute_command",
    description = [[Execute a shell command using vim.fn.jobstart and return the result.

Command Execution Guide:
1. Commands run in a separate process
2. Output is captured and returned when command completes
3. Environment is inherited from Neovim
4. Working directory must be specified]],

    inputSchema = {
        type = "object",
        properties = {
            command = {
                type = "string",
                description = "Shell command to execute",
                examples = { [["ls -la"]] },
            },
            cwd = {
                type = "string",
                description = "Working directory for the command",
                default = ".",
            },
        },
        required = { "command", "cwd" },
    },
    handler = function(req, res)
        local command = req.params.command
        local cwd = req.params.cwd

        if not command or command == "" then
            return res:error("command field is required and cannot be empty.")
        end

        if not cwd or cwd == "" then
            return res:error("cwd field is required and cannot be empty.")
        end

        -- Use Plenary Path to handle the path
        local path = Path:new(cwd)

        -- Check if the directory exists
        if not path:exists() then
            return res:error("Directory does not exist: " .. cwd)
        end

        -- Make sure it's a directory
        if not path:is_dir() then
            return res:error("Path is not a directory: " .. cwd)
        end

        local absolute_path = path:absolute()
        local output = ""
        local stderr_output = ""

        local options = {
            cwd = absolute_path,
            on_stdout = function(_, data)
                if data then
                    for _, line in ipairs(data) do
                        if line ~= "" then
                            output = output .. line .. "\n"
                        end
                    end
                end
            end,
            on_stderr = function(_, data)
                if data then
                    for _, line in ipairs(data) do
                        if line ~= "" then
                            stderr_output = stderr_output .. line .. "\n"
                        end
                    end
                end
            end,
            on_exit = function(_, exit_code)
                local result = ""

                -- Add command information
                result = result .. "Command: " .. command .. "\n"
                result = result .. "Working Directory: " .. absolute_path .. "\n"
                result = result .. "Exit Code: " .. exit_code .. "\n"

                -- Add stdout if there's any output
                if output ~= "" then
                    result = result .. "Output:\n\n" .. output
                end

                -- Add stderr if there's any error output
                if stderr_output ~= "" then
                    result = result .. "\nError Output:\n" .. stderr_output
                end

                -- If no output was captured at all
                if output == "" and stderr_output == "" then
                    result = result .. "Command completed with no output."
                end

                res:text(result):send()
            end,
        }

        -- Start the job
        local job_id = vim.fn.jobstart(command, options)

        if job_id <= 0 then
            local error_msg
            if job_id == 0 then
                error_msg = "Invalid arguments for jobstart"
            else -- job_id == -1
                error_msg = "Command is not executable"
            end
            return res:error(error_msg)
        end
    end,
})

local M = {}
local mcphub = require("mcphub")

local shared = require("mcphub.extensions.shared")

function M.register()
    local config = require("codecompanion.config")
    local hub = mcphub.get_hub_instance()
    if not hub then
        return
    end

    local prompts = hub:get_prompts()
    local slash_commands = config.strategies.chat.slash_commands

    -- Remove existing MCP slash commands
    for key, value in pairs(slash_commands) do
        local id = value.id or ""
        if id:sub(1, 3) == "mcp" then
            slash_commands[key] = nil
        end
    end

    -- Add current prompts as slash commands
    for _, prompt in ipairs(prompts) do
        local server_name = prompt.server_name
        local prompt_name = prompt.name or ""
        local description = prompt.description or ""
        description = description:gsub("\n", " ")
        description = prompt_name .. " (" .. description .. ")"

        local arguments = prompt.arguments or {}
        if type(arguments) == "function" then
            local ok, args = pcall(arguments, prompt)
            if ok then
                arguments = args or {}
            else
                vim.notify("Error in arguments function: " .. (args or ""), vim.log.levels.ERROR)
                arguments = {}
            end
        end

        slash_commands["mcp:" .. prompt_name] = {
            id = "mcp" .. server_name .. prompt_name,
            description = description,
            callback = function(self)
                shared.collect_arguments(arguments, function(values)
                    -- Sync call - blocks UI (can't use async in slash_commands yet)
                    local response, err = hub:get_prompt(server_name, prompt_name, values, {
                        caller = {
                            type = "codecompanion",
                            codecompanion = self,
                            meta = {
                                is_within_slash_command = true,
                            },
                        },
                        parse_response = true,
                    })

                    if not response then
                        if err then
                            vim.notify("Error in slash command: " .. err, vim.log.levels.ERROR)
                            vim.notify("Prompt cancelled", vim.log.levels.INFO)
                        end
                        return
                    end

                    local messages = response.messages or {}
                    local text_messages = 0

                    for i, message in ipairs(messages) do
                        local output = message.output
                        local mapped_role = message.role == "assistant" and config.constants.LLM_ROLE
                            or message.role == "system" and config.constants.SYSTEM_ROLE
                            or config.constants.USER_ROLE

                        if output.text and output.text ~= "" then
                            text_messages = text_messages + 1
                            -- If last message is from user, add it to chat buffer
                            if i == #messages and mapped_role == config.constants.USER_ROLE then
                                self:add_buf_message({
                                    role = mapped_role,
                                    content = output.text,
                                })
                            else
                                self:add_message({
                                    role = mapped_role,
                                    content = output.text,
                                })
                            end
                        end

                        -- Handle images
                        if output.images and #output.images > 0 then
                            local helpers = require("codecompanion.strategies.chat.helpers")
                            for _, image in ipairs(output.images) do
                                local id = string.format("mcp-%s", os.time())
                                helpers.add_image(self, {
                                    id = id,
                                    base64 = image.data,
                                    mimetype = image.mimeType,
                                }, { role = mapped_role })
                            end
                        end
                    end

                    vim.notify(
                        string.format(
                            "%s message%s added successfully",
                            text_messages,
                            text_messages == 1 and "" or "s"
                        ),
                        vim.log.levels.INFO
                    )
                end)
            end,
        }
    end
end

--- Setup MCP prompts as CodeCompanion slash commands
---@param opts MCPHub.Extensions.CodeCompanionConfig
function M.setup(opts)
    if not opts.make_slash_commands then
        return
    end

    vim.schedule(function()
        M.register()
    end)
    mcphub.on(
        { "servers_updated", "prompt_list_changed" },
        vim.schedule_wrap(function()
            M.register()
        end)
    )
end

return M

---@diagnostic disable: param-type-mismatch
--[[ Adapted from nui.text by Munif Tanjim
Source: https://github.com/MunifTanjim/nui.nvim/blob/main/lua/nui/text/init.lua
License: MIT ]]
local Object = {}
Object.__index = Object

---@class nui_text_extmark
---@field id? integer
---@field hl_group? string
---@field [string] any

local Text = Object

---@param content string|table text content or NuiText object
---@param extmark? string|nui_text_extmark highlight group name or extmark options
function Text:init(content, extmark)
    if type(content) == "string" then
        self:set(content, extmark)
    else
        -- cloning
        self:set(content._content, extmark or content.extmark)
    end
end

---@param content string text content
---@param extmark? string|nui_text_extmark highlight group name or extmark options
---@return table
function Text:set(content, extmark)
    if self._content ~= content then
        self._content = content
        self._length = vim.fn.strlen(content)
        self._width = vim.api.nvim_strwidth(content)
    end

    if extmark then
        -- preserve self.extmark.id
        local id = self.extmark and self.extmark.id or nil
        self.extmark = type(extmark) == "string" and {
            hl_group = extmark,
        } or vim.deepcopy(extmark)
        self.extmark.id = id
    end

    return self
end

---@return string
function Text:content()
    return self._content
end

---@return number
function Text:length()
    return self._length
end

---@return number
function Text:width()
    return self._width
end

---@param bufnr number buffer number
---@param ns_id number namespace id
---@param linenr number line number (1-indexed)
---@param start_col number start byte position (0-indexed)
function Text:highlight(bufnr, ns_id, linenr, start_col)
    if not self.extmark then
        return
    end

    start_col = start_col or 0
    self.extmark.end_col = start_col + self:length()

    self.extmark.id = vim.api.nvim_buf_set_extmark(bufnr, ns_id, linenr - 1, start_col, self.extmark)
end

---@param bufnr number buffer number
---@param ns_id number namespace id
---@param linenr_start number start line number (1-indexed)
---@param byte_start number start byte position (0-indexed)
---@param linenr_end? number end line number (1-indexed)
---@param byte_end? number end byte position (0-indexed)
function Text:render(bufnr, ns_id, linenr_start, byte_start, linenr_end, byte_end)
    local row_start = linenr_start - 1
    local row_end = linenr_end and linenr_end - 1 or row_start

    -- Start at column 0 if byte_start is nil
    local col_start = byte_start or 0
    -- If byte_end is nil, set it based on col_start and text length
    local col_end = byte_end or col_start + self:length()

    local content = self:content()

    -- Clear existing content first
    vim.api.nvim_buf_set_text(bufnr, row_start, col_start, row_end, col_start, { "" })
    -- Insert new content
    vim.api.nvim_buf_set_text(bufnr, row_start, col_start, row_start, col_start, { content })

    -- Highlight from col_start
    self:highlight(bufnr, ns_id, linenr_start, col_start)
end

-- Constructor
local NuiText = setmetatable({}, {
    __call = function(cls, content, extmark)
        local instance = setmetatable({}, Object)
        instance:init(content, extmark)
        return instance
    end,
})

return NuiText

local Path = require("plenary.path")

---Basic file operations tools
---@type MCPTool[]
local file_tools = {
    {
        name = "read_file",
        description = "Read contents of a file",
        inputSchema = {
            type = "object",
            properties = {
                path = {
                    type = "string",
                    description = "Path to the file to read",
                },
                start_line = {
                    type = "number",
                    description = "Start reading from this line (1-based index)",
                    default = 1,
                },
                end_line = {
                    type = "number",
                    description = "Read until this line (inclusive)",
                    default = -1,
                },
            },
            required = { "path" },
        },
        handler = function(req, res)
            local params = req.params
            local p = Path:new(params.path)

            if not p:exists() then
                return res:error("File not found: " .. params.path)
            end
            if params.start_line and params.end_line then
                params.start_line = tonumber(params.start_line)
                params.end_line = tonumber(params.end_line)
                if not params.start_line or not params.end_line then
                    return res:error("`start_line` and `end_line` must be numbers")
                end
                local extracted = {}
                local current_line = 0

                for line in p:iter() do
                    current_line = current_line + 1
                    if
                        current_line >= params.start_line and (params.end_line == -1 or current_line <= params.end_line)
                    then
                        table.insert(extracted, string.format("%4d │ %s", current_line, line))
                    end
                    if params.end_line ~= -1 and current_line > params.end_line then
                        break
                    end
                end
                return res:text(table.concat(extracted, "\n")):send()
            else
                return res:text(p:read()):send()
            end
        end,
    },
    {
        name = "move_item",
        description = "Move or rename a file/directory",
        inputSchema = {
            type = "object",
            properties = {
                path = {
                    type = "string",
                    description = "Source path",
                },
                new_path = {
                    type = "string",
                    description = "Destination path",
                },
            },
            required = { "path", "new_path" },
        },
        handler = function(req, res)
            local p = Path:new(req.params.path)
            if not p:exists() then
                return res:error("Source path not found: " .. req.params.path)
            end

            local new_p = Path:new(req.params.new_path)
            p:rename({ new_name = new_p.filename })
            return res:text(string.format("Moved %s to %s", req.params.path, req.params.new_path)):send()
        end,
    },
    {
        name = "read_multiple_files",
        description = "Read contents of multiple files in parallel. Prefer this tool when you need to view contents of more than one file at once.",
        inputSchema = {
            type = "object",
            properties = {
                paths = {
                    type = "array",
                    items = {
                        type = "string",
                    },
                    description = "Array of file paths to read",
                    examples = {
                        "file1.txt",
                        "/home/path/to/file2.txt",
                    },
                },
            },
            required = { "paths" },
        },
        handler = function(req, res)
            local params = req.params
            local results = {}
            local errors = {}

            if not params.paths or not vim.islist(params.paths) then
                return res:error("`paths` must be an array of strings. Provided " .. vim.inspect(params.paths))
            end

            if #params.paths == 0 then
                return res:error("`paths` array cannot be empty")
            end

            for i, path in ipairs(params.paths) do
                local p = Path:new(path)

                if not p:exists() then
                    table.insert(errors, string.format("File %d not found: %s", i, path))
                else
                    local success, content = pcall(function()
                        return p:read()
                    end)

                    if success then
                        table.insert(results, {
                            path = path,
                            content = content,
                            index = i,
                        })
                    else
                        table.insert(errors, string.format("Failed to read file %d (%s): %s", i, path, content))
                    end
                end
            end

            -- Format the response
            local response_parts = {}

            if #results > 0 then
                local file_word = #results == 1 and "file" or "files"
                table.insert(response_parts, string.format("Successfully read %d %s:\n", #results, file_word))

                for _, result in ipairs(results) do
                    table.insert(response_parts, string.format("=== File %d: %s ===", result.index, result.path))
                    table.insert(response_parts, result.content)
                    table.insert(response_parts, "") -- Empty line separator
                end
            end

            if #errors > 0 then
                if #results > 0 then
                    table.insert(response_parts, "\nErrors encountered:")
                end
                for _, error in ipairs(errors) do
                    table.insert(response_parts, "ERROR: " .. error)
                end
            end

            if #results == 0 and #errors > 0 then
                return res:error(table.concat(errors, "\n"))
            end

            return res:text(table.concat(response_parts, "\n")):send()
        end,
    },
    {
        name = "delete_items",
        description = "Delete multiple files or directories",
        inputSchema = {
            type = "object",
            properties = {
                paths = {
                    type = "array",
                    items = {
                        type = "string",
                    },
                    description = "Array of paths to delete",
                },
            },
            required = { "paths" },
        },
        handler = function(req, res)
            local params = req.params
            local results = {}
            local errors = {}

            if not params.paths or not vim.islist(params.paths) then
                return res:error("paths must be an array of strings")
            end

            if #params.paths == 0 then
                return res:error("paths array cannot be empty")
            end

            for i, path in ipairs(params.paths) do
                local p = Path:new(path)

                if not p:exists() then
                    table.insert(errors, string.format("Path %d not found: %s", i, path))
                else
                    local success, err = pcall(function()
                        p:rm()
                    end)

                    if success then
                        table.insert(results, {
                            path = path,
                            index = i,
                        })
                    else
                        table.insert(errors, string.format("Failed to delete path %d (%s): %s", i, path, err))
                    end
                end
            end

            -- Format the response
            local response_parts = {}

            if #results > 0 then
                local item_word = #results == 1 and "item" or "items"
                table.insert(response_parts, string.format("Successfully deleted %d %s:", #results, item_word))

                for _, result in ipairs(results) do
                    table.insert(response_parts, string.format("  %d. %s", result.index, result.path))
                end
            end

            if #errors > 0 then
                if #results > 0 then
                    table.insert(response_parts, "\nErrors encountered:")
                end
                for _, error in ipairs(errors) do
                    table.insert(response_parts, "ERROR: " .. error)
                end
            end

            if #results == 0 and #errors > 0 then
                return res:error(table.concat(errors, "\n"))
            end

            return res:text(table.concat(response_parts, "\n")):send()
        end,
    },
}

return file_tools

local Error = require("mcphub.utils.errors")
local ImageCache = require("mcphub.utils.image_cache")
local Job = require("plenary.job")
local MCPHub = require("mcphub.hub")
local State = require("mcphub.state")
local log = require("mcphub.utils.log")
local native = require("mcphub.native")
local utils = require("mcphub.utils")
local validation = require("mcphub.utils.validation")

-- Initialize global variables for lualine integration
vim.g.mcphub_status = "stopped"
vim.g.mcphub_servers_count = 0
vim.g.mcphub_executing = false

---@class MCPHub
local M = {
    is_native_server = native.is_native_server,
    add_server = native.add_server,
    add_tool = native.add_tool,
    add_resource = native.add_resource,
    add_resource_template = native.add_resource_template,
    add_prompt = native.add_prompt,
}

---Get the current MCPHub instance
---@return MCPHub.Hub | nil
function M.get_hub_instance()
    if State.setup_state ~= "completed" then
        return nil
    end
    return State.hub_instance
end

---Get the current state of the MCPHub
---@return MCPHub.State
function M.get_state()
    return State
end

---@param event string | table
---@param callback fun(data: table)
function M.on(event, callback)
    --if event is an array then add each event
    if type(event) == "table" then
        for _, e in ipairs(event) do
            State:add_event_listener(e, callback)
        end
        return
    end
    State:add_event_listener(event, callback)
end

---@param event string | table
---@param callback fun(data: table)
function M.off(event, callback)
    --if event is an array then remove each event
    if type(event) == "table" then
        for _, e in ipairs(event) do
            State:remove_event_listener(e, callback)
        end
        return
    end
    State:remove_event_listener(event, callback)
end

--- Setup MCPHub plugin with error handling and validation
--- @param opts MCPHub.Config?
---@return MCPHub.Hub | nil
function M.setup(opts)
    ---@param err MCPError
    local function _on_setup_failed(err)
        if err then
            State:add_error(err)
            State:update({
                setup_state = "failed",
            }, "setup")
            State.config.on_error(tostring(err))
        end
    end

    ---Version check handler
    ---@param job Job
    ---@param code number
    ---@param config MCPHub.Config
    local function _handle_version_check(job, code, config)
        if code ~= 0 then
            return _on_setup_failed(
                Error(
                    "SETUP",
                    Error.Types.SETUP.MISSING_DEPENDENCY,
                    "mcp-hub exited with non-zero code. Please verify your installation."
                )
            )
        end

        -- Validate version
        local version_result = validation.validate_version(job:result()[1])
        if not version_result.ok then
            return _on_setup_failed(version_result.error)
        end

        -- Create hub instance
        local hub = MCPHub:new(config)
        if not hub then
            return _on_setup_failed(Error("SETUP", Error.Types.SETUP.SERVER_START, "Failed to create MCPHub instance"))
        end

        State.hub_instance = hub
        State:update({
            setup_state = "completed",
        }, "setup")

        -- Initialize image cache
        ImageCache.setup()

        -- Setup Extensions
        require("mcphub.extensions").setup(config.extensions)
        -- Start hub
        hub:start()
    end
    -- Return if already setup or in progress
    if State.setup_state ~= "not_started" then
        return State.hub_instance
    end
    -- Update state to in_progress
    State:update({
        setup_state = "in_progress",
    }, "setup")
    local config = require("mcphub.config").setup(opts)
    local cmds = utils.get_default_cmds(config)
    config.cmd = cmds.cmd
    config.cmdArgs = cmds.cmdArgs
    if type(config.auto_approve) == "boolean" and config.auto_approve == true then
        vim.g.mcphub_auto_approve = vim.g.mcphub_auto_approve == nil and true or vim.g.mcphub_auto_approve
    end
    log.setup(config.log)
    State.ui_instance = require("mcphub.ui"):new(config.ui)
    State.config = config

    -- Validate options
    local validation_result = validation.validate_setup_opts(config)
    if not validation_result.ok then
        return _on_setup_failed(validation_result.error)
    end

    -- Setup cleanup and directory change handling
    local group = vim.api.nvim_create_augroup("mcphub_cleanup", {
        clear = true,
    })
    vim.api.nvim_create_autocmd("VimLeavePre", {
        group = group,
        callback = function()
            if State.hub_instance then
                State.hub_instance:stop()
            end
            -- UI cleanup is handled by its own autocmd
        end,
    })

    -- Handle directory changes for workspace-aware mode
    if config.workspace.enabled and config.workspace.reload_on_dir_changed then
        vim.api.nvim_create_autocmd("DirChanged", {
            group = group,
            callback = vim.schedule_wrap(function()
                if State.hub_instance then
                    -- Small delay to ensure directory change is complete
                    vim.defer_fn(function()
                        State.hub_instance:handle_directory_change()
                    end, 100)
                end
            end),
        })
    end

    -- Start version check
    local ok, job = pcall(function()
        ---@diagnostic disable-next-line: missing-fields
        return Job:new({
            command = config.cmd,
            args = utils.clean_args({ config.cmdArgs, "--version" }),
            on_exit = vim.schedule_wrap(function(j, code)
                _handle_version_check(j, code, config)
            end),
        })
    end)

    local help_msg = [[mcp-hub executable not found. Please ensure:
1. For global install: Run 'npm install -g mcp-hub@latest'
2. For bundled install: Set build = 'bundled_build.lua' in lazy spec and use_bundled_binary = true in config.
3. For custom install: Verify cmd/cmdArgs point to valid mcp-hub executable
]]
    if not ok then
        -- Handle executable not found error
        return _on_setup_failed(Error("SETUP", Error.Types.SETUP.MISSING_DEPENDENCY, help_msg, { stack = job }))
    end

    -- Start the job (uv.spawn might fail)
    local spawn_ok, err = pcall(job.start, job)
    if not spawn_ok then
        -- Handle spawn error
        return _on_setup_failed(
            Error(
                "SETUP",
                Error.Types.SETUP.MISSING_DEPENDENCY,
                "Failed to spawn mcp-hub process: " .. tostring(err) .. "\n\n" .. help_msg
            )
        )
    end

    return State.hub_instance
end

return M

local Path = require("plenary.path")
local Text = require("mcphub.utils.text")
local scan = require("plenary.scandir")

-- Get file info utility
local function get_file_info(path)
    local fullpath = vim.fn.expand(path)
    local stat = vim.loop.fs_stat(fullpath)
    if not stat then
        return nil, "File not found: " .. path
    end

    return {
        name = vim.fn.fnamemodify(fullpath, ":t"),
        path = fullpath,
        size = stat.size,
        type = stat.type,
        modified = stat.mtime.sec,
        permissions = stat.mode,
        is_readonly = not vim.loop.fs_access(fullpath, "W"),
    }
end

---@type MCPTool[]
local search_tools = {
    {
        name = "find_files",
        description = "Search for files by pattern",
        inputSchema = {
            type = "object",
            properties = {
                pattern = {
                    type = "string",
                    description = "Search pattern (e.g. *.lua)",
                },
                path = {
                    type = "string",
                    description = "Directory to search in",
                    default = ".",
                },
                recursive = {
                    type = "boolean",
                    description = "Search recursively",
                    default = true,
                },
            },
            required = { "pattern" },
        },
        handler = function(req, res)
            local params = req.params
            if not params.pattern or params.pattern == "" then
                return res:error("Pattern is required for file search")
            end
            -- local path = vim.fn.expand(params.path or ".")
            local path = Path:new(params.path or "."):absolute()
            local pattern = params.pattern

            -- Build glob pattern
            local glob = vim.fn.fnamemodify(path, ":p")
            if params.recursive then
                glob = glob .. "**/"
            end
            glob = glob .. pattern

            -- Find files
            local files = vim.fn.glob(glob, true, true)
            if #files == 0 then
                return res:text("No files found matching: " .. pattern):send()
            end

            -- Get file info
            local results = {}
            for _, file in ipairs(files) do
                local ok, info = pcall(get_file_info, file)
                if ok and info then
                    table.insert(results, info)
                end
            end

            -- Format results
            local text = string.format("%s Search Results: %s\n%s\n", Text.icons.search, pattern, string.rep("-", 40))

            for _, info in ipairs(results) do
                local icon = info.type == "directory" and Text.icons.folder or Text.icons.file
                text = text .. string.format("%s %s\n", icon, info.path)
            end

            text = text .. string.format("\nFound %d matches", #results)
            return res:text(text):send()
        end,
    },
    {
        name = "list_directory",
        description = "List files and directories in a path",
        inputSchema = {
            type = "object",
            properties = {
                path = {
                    type = "string",
                    description = "Directory path to list",
                    default = ".",
                },
            },
        },
        handler = function(req, res)
            local params = req.params
            local path = Path:new(params.path or "."):absolute()
            local depth = nil
            local hidden = false
            local respect_gitignore = true
            local include_dirs = false
            local scan_opts = {
                hidden = hidden,
                depth = depth,
                respect_gitignore = respect_gitignore,
                add_dirs = include_dirs,
            }

            local results = scan.scan_dir(path, scan_opts)

            if #results == 0 then
                return res:text("No files found in: " .. path):send()
            end

            -- Get file info for each result
            local file_results = {}
            for _, file in ipairs(results) do
                local ok, info = pcall(get_file_info, file)
                if ok and info then
                    table.insert(file_results, info)
                end
            end

            -- Format results
            local text = string.format("%s Directory Listing: %s\n%s\n", Text.icons.folder, path, string.rep("-", 40))

            for _, info in ipairs(file_results) do
                local icon = info.type == "directory" and Text.icons.folder or Text.icons.file
                local relative_path = info.path:sub(#path + 2)
                text = text .. string.format("%s %s\n", icon, relative_path)
            end

            text = text .. string.format("\nFound %d items", #file_results)
            return res:text(text):send()
        end,
    },
}

return search_tools

local types = require("mcphub.native.neovim.files.edit_file.types")

---@class BlockLocator
---@field config LocatorConfig Configuration options
---@field search_engine SearchEngine Search engine instance
local BlockLocator = {}
BlockLocator.__index = BlockLocator

-- Default locator configuration
local DEFAULT_CONFIG = {
    fuzzy_threshold = 0.8, -- Minimum similarity score for fuzzy matches
    enable_fuzzy_matching = true, -- Allow fuzzy matching when exact fails
}

-- Create new block locator instance
---@param config LocatorConfig? Optional configuration
---@return BlockLocator
function BlockLocator.new(config)
    local self = setmetatable({}, BlockLocator)
    self.config = vim.tbl_deep_extend("force", DEFAULT_CONFIG, config or {})
    local SearchEngine = require("mcphub.native.neovim.files.edit_file.search_engine")
    self.search_engine = SearchEngine.new(self.config)
    return self
end

-- Locate all blocks in file content
---@param parsed_blocks ParsedBlock[] Blocks from DiffParser
---@param file_content string Content of the target file
---@return LocatedBlock[] located_blocks Blocks with location information
function BlockLocator:locate_all_blocks(parsed_blocks, file_content)
    local file_lines = vim.split(file_content, "\n", { plain = true, trimempty = true })
    local located_blocks = {}
    for _, parsed_block in ipairs(parsed_blocks) do
        if vim.trim(parsed_block.search_content) == "" then
            parsed_block.search_content = file_content
            parsed_block.search_lines = file_lines
        end

        local location_result = self.search_engine:locate_block_in_file(parsed_block.search_lines, file_lines)

        ---@type LocatedBlock
        local located_block = {
            search_content = parsed_block.search_content,
            replace_content = parsed_block.replace_content,
            block_id = parsed_block.block_id,
            search_lines = parsed_block.search_lines,
            replace_lines = parsed_block.replace_lines,
            location_result = location_result,
        }
        table.insert(located_blocks, located_block)
    end

    return located_blocks
end

-- Get comprehensive feedback including location statistics
---@param located_blocks? LocatedBlock[] Processed blocks for statistics
---@return string? feedback Complete feedback for LLM
function BlockLocator:get_feedback(located_blocks)
    local search_logs = {}
    if located_blocks then
        for _, block in ipairs(located_blocks) do
            if not block.location_result.found then
                local found_content = block.location_result.found_content
                local best_match = ""
                local best_score = block.location_result.confidence
                if found_content then
                    best_match = string.format(
                        "\nThe following is the BEST MATCH found from Line `%s` to `%s` with `%s%%` confidence:\n",
                        block.location_result.start_line or "N/A",
                        block.location_result.end_line or "N/A",
                        best_score
                    )
                    best_match = best_match
                        .. string.format(
                            -- '<SEARCHED-CONTENT>\n%s\n</SEARCHED-CONTENT>\n<BESTMATCH confidence="%s%%" startline="%s" endline="%s">\n%s\n</BESTMATCH>',
                            '<BESTMATCH confidence="%s%%" startline="%s" endline="%s">\n%s\n</BESTMATCH>',
                            -- block.search_content,
                            best_score,
                            block.location_result.start_line or "N/A",
                            block.location_result.end_line or "N/A",
                            found_content
                        )
                else
                    best_match = ""
                end
                table.insert(
                    search_logs,
                    string.format(
                        [[
### `%s` (ERROR)
Finding SEARCH content from `%s` failed with error: `%s`%s]],
                        block.block_id,
                        block.block_id,
                        block.location_result.error,
                        best_match
                    )
                )
            end
            local is_fuzzy = block.location_result.found
                and (
                    block.location_result.overall_match_type ~= types.OVERALL_MATCH_TYPE.exact
                    and block.location_result.overall_match_type ~= types.OVERALL_MATCH_TYPE.exact_whitespace
                )
            if is_fuzzy then
                local diff = vim.diff(block.location_result.found_content, block.search_content, {
                    result_type = "unified",
                }) --[[@as string?]]
                table.insert(
                    search_logs,
                    string.format(
                        [[
### `%s` (WARNING)
SEARCH content from `%s` is fuzzily matched and applied at LINES `%s` to `%s` with `%.2f%%` confidence:
The following is a diff for `%s` SEARCH content vs Fuzzily matched content:
```diff
%s
```]],
                        block.block_id,
                        block.block_id,
                        block.location_result.start_line,
                        block.location_result.end_line,
                        block.location_result.confidence,
                        block.block_id,
                        vim.trim(diff or "")
                    )
                )
            end
        end
    end
    if #search_logs > 0 then
        return "## ISSUES WHILE SEARCHING\n" .. table.concat(search_logs, "\n\n")
    end
end

return BlockLocator

local M = {}
local mcphub = require("mcphub")

---@param opts MCPHub.Extensions.CodeCompanionConfig
function M.register(opts)
    local hub = mcphub.get_hub_instance()
    if not hub then
        return
    end

    local resources = hub:get_resources()
    local ok, config = pcall(require, "codecompanion.config")
    if not ok then
        return
    end

    local cc_variables = config.strategies.chat.variables

    -- Remove existing MCP variables
    for key, value in pairs(cc_variables) do
        local id = value.id or ""
        if id:sub(1, 3) == "mcp" then
            cc_variables[key] = nil
        end
    end

    local added_resources = {}
    -- Add current resources as variables
    for _, resource in ipairs(resources) do
        local server_name = resource.server_name
        local uri = resource.uri
        local resource_name = resource.name or uri
        local description = resource.description or ""
        description = description:gsub("\n", " ")
        description = resource_name .. " (" .. description .. ")"
        local var_id = "mcp:" .. uri
        cc_variables[var_id] = {
            id = "mcp" .. server_name .. uri,
            description = description,
            hide_in_help_window = true,
            callback = function(self)
                -- Sync call - blocks UI (can't use async in variables yet)
                local result = hub:access_resource(server_name, uri, {
                    caller = {
                        type = "codecompanion",
                        codecompanion = self,
                        meta = {
                            is_within_variable = true,
                        },
                    },
                    parse_response = true,
                })

                if not result then
                    return string.format("Accessing resource failed: %s", uri)
                end

                -- Handle images
                if result.images and #result.images > 0 then
                    local helpers = require("codecompanion.strategies.chat.helpers")
                    for _, image in ipairs(result.images) do
                        local id = string.format("mcp-%s", os.time())
                        helpers.add_image(self.Chat, {
                            id = id,
                            base64 = image.data,
                            mimetype = image.mimeType,
                        })
                    end
                end

                return result.text
            end,
        }
        table.insert(added_resources, var_id)
    end

    -- Update syntax highlighting for variables
    M.update_variable_syntax(added_resources)
end
-- Setup MCP resources as CodeCompanion variables
---@param opts MCPHub.Extensions.CodeCompanionConfig
function M.setup(opts)
    if not opts.make_vars then
        return
    end
    vim.schedule(function()
        M.register(opts)
    end)
    mcphub.on(
        { "servers_updated", "resource_list_changed" },
        vim.schedule_wrap(function()
            M.register(opts)
        end)
    )
end

--- Update syntax highlighting for variables
---@param resources string[]
function M.update_variable_syntax(resources)
    vim.schedule(function()
        for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
            if vim.api.nvim_buf_is_valid(bufnr) and vim.bo[bufnr].filetype == "codecompanion" then
                vim.api.nvim_buf_call(bufnr, function()
                    for _, resource in ipairs(resources) do
                        vim.cmd.syntax('match CodeCompanionChatVariable "#{' .. resource .. '}"')
                        vim.cmd.syntax('match CodeCompanionChatVariable "#{' .. resource .. '}{[^}]*}"')
                    end
                end)
            end
        end
    end)
end

return M

---@module "avante"
--[[
*MCP Servers Tool*
This tool can be used to call tools and resources from the MCP Servers.
M.mcp_tool() will return a use_mcp_tool and access_mcp_resource function schemas.

M.use_mcp_tool() will return schema for calling tools on MCP servers.
M.access_mcp_resource() will return schema for accessing resources on MCP servers.
--]]

---@class MCPHub.Extensions.Avante
---@field setup function(config: MCPHub.Extensions.AvanteConfig) Setup slash commands and other configurations for Avante extension
---@field mcp_tool fun(): AvanteLLMTool,AvanteLLMTool
---@field use_mcp_tool AvanteLLMTool
---@field access_mcp_resource AvanteLLMTool

---@class MCPHub.Extensions.Avante
local M = {}

---@type table<MCPHub.ActionType, AvanteLLMTool>
local tool_schemas = {
    use_mcp_tool = {
        name = "use_mcp_tool",
        description = "Calls tools on MCP servers.",
        param = {
            type = "table",
            fields = {
                {
                    name = "server_name",
                    description = "Name of the server to call the tool on. Must be from one of the available servers.",
                    type = "string",
                },
                {
                    name = "tool_name",
                    description = "Name of the tool to call.",
                    type = "string",
                },
                {
                    name = "tool_input",
                    description = "Input for the tool call. Must be a valid JSON object.",
                    type = "object",
                },
            },
        },
        returns = {}, -- Will be added dynamically in mcp_tool()
    },

    access_mcp_resource = {
        name = "access_mcp_resource",
        description = "Get resources on MCP servers.",
        param = {
            type = "table",
            fields = {
                {
                    name = "server_name",
                    description = "Name of the server to call the resource on. Must be from one of the available servers.",
                    type = "string",
                },
                {
                    name = "uri",
                    description = "URI of the resource to access.",
                    type = "string",
                },
            },
        },
        returns = {}, -- Will be added dynamically in mcp_tool()
    },
}

---@return AvanteLLMTool use_mcp_tool
---@return AvanteLLMTool access_mcp_resource
function M.mcp_tool()
    ---@param str string
    ---@param max_length number
    ---@return string
    local function truncate_utf8(str, max_length)
        if type(str) ~= "string" or #str <= max_length then
            return str
        end
        local i = 1
        local bytes = #str
        while i <= bytes and i < max_length do
            local c = string.byte(str, i)
            if c < 0x80 then
                i = i + 1
            elseif c < 0xE0 then
                i = i + 2
            elseif c < 0xF0 then
                i = i + 3
            else
                i = i + 4
            end
        end
        return str:sub(1, i - 1) .. "... (truncated)"
    end

    local async = require("plenary.async")
    local shared = require("mcphub.extensions.shared")
    for action_name, schema in pairs(tool_schemas) do
        ---@type AvanteLLMToolFunc<MCPHub.ToolCallArgs | MCPHub.ResourceAccessArgs>
        schema.func = function(args, opts)
            opts = opts or {}
            local on_complete = opts.on_complete or function() end
            local on_log = opts.on_log or function() end
            ---@diagnostic disable-next-line: missing-parameter
            async.run(function()
                local hub = require("mcphub").get_hub_instance()
                if not hub then
                    return on_complete(nil, "MCP Hub not initialized")
                end
                local params = shared.parse_params(args, action_name)
                if #params.errors > 0 then
                    return on_complete(nil, table.concat(params.errors, "\n"))
                end

                local result = shared.handle_auto_approval_decision(params)
                if result.error then
                    return on_complete(nil, result.error)
                end
                local sidebar = require("avante").get()
                if params.action == "access_mcp_resource" then
                    if on_log and type(on_log) == "function" then
                        on_log(
                            string.format("Accessing `%s` resource from server `%s`", params.uri, params.server_name)
                        )
                    end
                    hub:access_resource(params.server_name, params.uri, {
                        parse_response = true,
                        caller = {
                            type = "avante",
                            avante = sidebar,
                            auto_approve = result.approve,
                        },
                        callback = function(result, err)
                            --result has .text and .images [{mimeType, data}]
                            on_complete(result.text, err)
                        end,
                    })
                elseif params.action == "use_mcp_tool" then
                    if on_log and type(on_log) == "function" then
                        on_log(
                            string.format(
                                "Calling tool `%s` on server `%s` with arguments: %s",
                                params.tool_name,
                                params.server_name,
                                vim.inspect(params.arguments, {
                                    indent = "  ",
                                    depth = 2,
                                    process = function(item)
                                        return truncate_utf8(item, 80)
                                    end,
                                })
                            )
                        )
                    end
                    hub:call_tool(params.server_name, params.tool_name, params.arguments, {
                        parse_response = true,
                        caller = {
                            type = "avante",
                            avante = sidebar,
                            auto_approve = result.approve,
                        },
                        callback = function(result, err)
                            if result.error then
                                on_complete(nil, result.error)
                            else
                                on_complete(result.text, err)
                            end
                        end,
                    })
                else
                    return on_complete(nil, "Invalid action type")
                end
            end)
        end

        ---@type AvanteLLMToolReturn[]
        schema.returns = {
            {
                name = "result",
                description = string.format("The `%s` call returned the following text:\n", action_name),
                type = "string",
            },
            {
                name = "error",
                description = string.format("The `%s` call failed with the following error:\n", action_name),
                type = "string",
                optional = true,
            },
        }
        M[action_name] = schema
    end
    ---@diagnostic disable-next-line: redundant-return-value
    return unpack(vim.tbl_values(tool_schemas))
end

---@param config MCPHub.Extensions.AvanteConfig
function M.setup(config)
    if config.make_slash_commands then
        --Avoid checking for avante if the extension is not enabled
        local ok, _ = pcall(require, "avante")
        if not ok then
            return
        end
        require("mcphub.extensions.avante.slash_commands").setup()
    end
end

return M

local mcphub = require("mcphub")
local shared = require("mcphub.extensions.shared")

local M = {}

---Update avante slash commands with the current MCP prompts.
function M.register()
    local ok, avante_config = pcall(require, "avante.config")
    if not ok then
        return
    end

    local hub = mcphub.get_hub_instance()
    if not hub then
        return
    end
    local prompts = hub:get_prompts()
    local avante_slash_commands = avante_config.slash_commands or {}
    -- remove existing mcp slash commands that start with mcp so that when user disables a server, those prompts are removed
    for i, value in ipairs(avante_slash_commands) do
        local id = value.name or ""
        if id:sub(1, 3) == "mcp" then
            avante_slash_commands[i] = nil
        end
    end
    --add all prompts from MCP servers
    for _, prompt in ipairs(prompts) do
        local server_name = prompt.server_name
        local prompt_name = prompt.name or ""
        local description = prompt.description or ""
        local arguments = prompt.arguments or {}
        if type(description) == "function" then
            local desc_ok, desc = pcall(description, prompt)
            if desc_ok then
                description = desc or ""
            else
                description = "Error in description function: " .. (desc or "")
            end
        end
        if type(arguments) == "function" then
            local args_ok, args = pcall(arguments, prompt)
            if args_ok then
                arguments = args or {}
            else
                vim.notify("Error in arguments function: " .. (args or ""), vim.log.levels.ERROR)
                arguments = {}
            end
        end
        --remove new lines
        description = description:gsub("\n", " ")

        description = prompt_name .. " (" .. description .. ")"
        local slash_command = {
            name = "mcp:" .. server_name .. ":" .. prompt_name,
            description = description,
            callback = function(sidebar, args, cb)
                shared.collect_arguments(arguments, function(values)
                    local response, err = hub:get_prompt(server_name, prompt_name, values, {
                        caller = {
                            type = "avante",
                            avante = sidebar,
                            meta = {
                                is_within_slash_command = true,
                            },
                        },
                        parse_response = true,
                    })
                    if not response then
                        if err then
                            vim.notify("Error in slash command: " .. err, vim.log.levels.ERROR)
                            vim.notify("Prompt cancelled", vim.log.levels.INFO)
                        end
                        return
                    end
                    local messages = response.messages or {}
                    local text_messages = {}
                    for i, message in ipairs(messages) do
                        local output = message.output
                        if output.text and output.text ~= "" then
                            if i == #messages and message.role == "user" then
                                sidebar:set_input_value(output.text)
                            else
                                table.insert(text_messages, {
                                    role = message.role,
                                    content = output.text,
                                })
                            end
                        end
                    end
                    sidebar:add_chat_history(text_messages, { visible = true })
                    vim.notify(
                        string.format(
                            "%s message%s added successfully",
                            #text_messages,
                            #text_messages == 1 and "" or "s"
                        ),
                        vim.log.levels.INFO
                    )
                    if cb then
                        cb()
                    end
                end)
            end,
        }
        table.insert(avante_slash_commands, slash_command)
    end
end

---Add slash commands to Avante for all active MCP servers
function M.setup()
    ---Immediately register slash commands if enabled
    M.register()
    --setup event listeners to update slash commands
    mcphub.on({ "servers_updated", "prompt_list_changed" }, M.register)
end

return M

local M = {}
local async = require("plenary.async")
local shared = require("mcphub.extensions.shared")

--- Core MCP tool execution logic
---@param params MCPHub.ToolCallArgs | MCPHub.ResourceAccessArgs
---@param agent CodeCompanion.Agent The Editor tool
---@param output_handler function Callback for asynchronous calls
---@param context MCPHub.ToolCallContext
---@return nil|{ status: "success"|"error", data: string }
function M.execute_mcp_tool(params, agent, output_handler, context)
    context = context or {}
    ---@diagnostic disable-next-line: missing-parameter
    async.run(function()
        -- Reuse existing validation logic
        local parsed_params = shared.parse_params(params, context.action)
        if #parsed_params.errors > 0 then
            return output_handler({
                status = "error",
                data = table.concat(parsed_params.errors, "\n"),
            })
        end

        local result = shared.handle_auto_approval_decision(parsed_params)

        if result.error then
            return output_handler({
                status = "error",
                data = result.error,
            })
        end

        local hub = require("mcphub").get_hub_instance()
        if not hub then
            return output_handler({
                status = "error",
                data = "MCP Hub is not ready yet",
            })
        end

        -- Call appropriate hub method
        if parsed_params.action == "access_mcp_resource" then
            hub:access_resource(parsed_params.server_name, parsed_params.uri, {
                caller = {
                    type = "codecompanion",
                    codecompanion = agent,
                    auto_approve = result.approve,
                },
                parse_response = true,
                callback = function(res, err)
                    if err or not res then
                        output_handler({
                            status = "error",
                            data = err and tostring(err)
                                or "No response from accessing the resource " .. parsed_params.uri,
                        })
                    elseif res then
                        output_handler({ status = "success", data = res })
                    end
                end,
            })
        elseif parsed_params.action == "use_mcp_tool" then
            hub:call_tool(parsed_params.server_name, parsed_params.tool_name, parsed_params.arguments, {
                caller = {
                    type = "codecompanion",
                    codecompanion = agent,
                    auto_approve = result.approve,
                },
                parse_response = true,
                callback = function(res, err)
                    if err or not res then
                        output_handler({ status = "error", data = tostring(err) or "No response from tool call" })
                    elseif res.error then
                        output_handler({ status = "error", data = res.error })
                    else
                        output_handler({ status = "success", data = res })
                    end
                end,
            })
        else
            return output_handler({
                status = "error",
                data = "Invalid action type: " .. parsed_params.action,
            })
        end
    end)
end

---@param display_name string
---@param tool CodeCompanion.Agent.Tool
---@param chat any
---@param llm_msg string
---@param is_error boolean
---@param has_function_calling boolean
---@param opts MCPHub.Extensions.CodeCompanionConfig
---@param user_msg string?
---@param images table
-- Helper function for tool output formatting
local function add_tool_output(
    display_name,
    tool,
    chat,
    llm_msg,
    is_error,
    has_function_calling,
    opts,
    user_msg,
    images
)
    local config = require("codecompanion.config")
    local helpers = require("codecompanion.strategies.chat.helpers")
    local show_result_in_chat = opts.show_result_in_chat == true
    local text = llm_msg
    local formatted_name = opts.format_tool and opts.format_tool(display_name, tool) or display_name

    if has_function_calling then
        chat:add_tool_output(
            tool,
            text,
            (user_msg or show_result_in_chat or is_error) and (user_msg or text)
                or string.format("**`%s` Tool**: Successfully finished", formatted_name)
        )
        for _, image in ipairs(images) do
            helpers.add_image(chat, image)
        end
    else
        if show_result_in_chat or is_error then
            chat:add_buf_message({
                role = config.constants.USER_ROLE,
                content = text,
            })
        else
            chat:add_message({
                role = config.constants.USER_ROLE,
                content = text,
            })
            chat:add_buf_message({
                role = config.constants.USER_ROLE,
                content = string.format("I've shared the result of the `%s` tool with you.\n", formatted_name),
            })
        end
    end
end

---@param display_name string
---@param has_function_calling boolean
---@param opts MCPHub.Extensions.CodeCompanionConfig
---@return {error: function, success: function}
function M.create_output_handlers(display_name, has_function_calling, opts)
    return {
        ---@param self CodeCompanion.Agent.Tool
        ---@param agent CodeCompanion.Agent
        ---@param stderr table The error output from the command
        error = function(self, agent, cmd, stderr)
            ---@diagnostic disable-next-line: cast-local-type
            stderr = has_function_calling and (stderr[#stderr] or "") or cmd[#cmd]
            ---@diagnostic disable-next-line: cast-local-type
            agent = has_function_calling and agent or self
            if type(stderr) == "table" then
                ---@diagnostic disable-next-line: cast-local-type
                stderr = vim.inspect(stderr)
            end
            local formatted_name = opts.format_tool and opts.format_tool(display_name, self) or display_name
            local err_msg = string.format(
                [[**`%s` Tool**: Failed with the following error:

````
%s
````
]],
                formatted_name,
                stderr
            )
            add_tool_output(display_name, self, agent.chat, err_msg, true, has_function_calling, opts, nil, {})
        end,

        ---@param self CodeCompanion.Agent.Tool
        ---@param agent CodeCompanion.Agent
        ---@param cmd table The command that was executed
        ---@param stdout table The output from the command
        success = function(self, agent, cmd, stdout)
            local image_cache = require("mcphub.utils.image_cache")
            ---@type MCPResponseOutput
            local result = has_function_calling and stdout[#stdout] or cmd[#cmd]
            ---@diagnostic disable-next-line: cast-local-type
            agent = has_function_calling and agent or self
            local formatted_name = opts.format_tool and opts.format_tool(display_name, self) or display_name
            local to_llm = nil
            local to_user = nil
            local images = {}

            if result.text and result.text ~= "" then
                to_llm = string.format(
                    [[**`%s` Tool**: Returned the following:

````
%s
````]],
                    formatted_name,
                    result.text
                )
            end

            if result.images and #result.images > 0 then
                for _, image in ipairs(result.images) do
                    local cached_file_path = image_cache.save_image(image.data, image.mimeType)
                    local id = cached_file_path
                    table.insert(images, {
                        id = id,
                        base64 = image.data,
                        mimetype = image.mimeType,
                        cached_file_path = cached_file_path,
                    })
                end

                if not to_llm then
                    to_llm = string.format(
                        [[**`%s` Tool**: Returned the following:
````
%s
````]],
                        formatted_name,
                        string.format("%d image%s returned", #result.images, #result.images > 1 and "s" or "")
                    )
                end

                to_user = to_llm .. (#images > 0 and string.format("\n\n#### Preview Images\n") or "")
                for _, image in ipairs(images) do
                    local file = image.cached_file_path
                    if file then
                        local file_name = vim.fn.fnamemodify(file, ":t")
                        to_user = to_user .. string.format("\n![%s](%s)\n", file_name, vim.fn.fnameescape(file))
                    else
                        to_user = to_user .. string.format("\n![Image not saved properly](%s)\n", file)
                    end
                end
            end

            local fallback_to_llm = string.format("**`%s` Tool**: Completed with no output", formatted_name)
            if opts.show_result_in_chat == false and not to_user then
                to_user = string.format("**`%s` Tool**: Successfully finished", formatted_name)
            end
            add_tool_output(
                display_name,
                self,
                agent.chat,
                to_llm or fallback_to_llm,
                false,
                has_function_calling,
                opts,
                to_user,
                images
            )
        end,
    }
end

return M

-- String utilities for normalization and comparison
-- Used by the enhanced search engine for fuzzy matching

local M = {}

-- Character mappings for comprehensive normalization
local NORMALIZATION_MAPS = {
    -- Smart quotes to regular quotes
    SMART_QUOTES = {
        ["\u{201C}"] = '"', -- Left double quote (U+201C)
        ["\u{201D}"] = '"', -- Right double quote (U+201D)
        ["\u{2018}"] = "'", -- Left single quote (U+2018)
        ["\u{2019}"] = "'", -- Right single quote (U+2019)
        ['"'] = '"', -- Alternative left double quote
        ['"'] = '"', -- Alternative right double quote
        ["'"] = "'", -- Alternative left single quote
        ["'"] = "'", -- Alternative right single quote
    },

    -- Typographic characters
    TYPOGRAPHIC = {
        ["\u{2026}"] = "...", -- Ellipsis
        ["\u{2014}"] = "-", -- Em dash
        ["\u{2013}"] = "-", -- En dash
        ["\u{00A0}"] = " ", -- Non-breaking space
        ["…"] = "...", -- Ellipsis (alternative)
        ["—"] = "-", -- Em dash (alternative)
        ["–"] = "-", -- En dash (alternative)
    },

    -- HTML entities (common in copied code)
    HTML_ENTITIES = {
        ["<"] = "<",
        [">"] = ">",
        ["""] = '"',
        ["'"] = "'",
        ["'"] = "'",
        ["&"] = "&",
    },
}

-- Normalization options
---@class NormalizeOptions
---@field smart_quotes boolean Replace smart quotes with straight quotes
---@field typographic_chars boolean Replace typographic characters
---@field html_entities boolean Unescape HTML entities
---@field extra_whitespace boolean Collapse multiple whitespace to single space
---@field trim boolean Trim whitespace from start and end
---@field normalize_case boolean Convert to lowercase for comparison

local DEFAULT_NORMALIZE_OPTIONS = {
    smart_quotes = true,
    typographic_chars = true,
    html_entities = true,
    extra_whitespace = true,
    trim = true,
    normalize_case = false, -- Keep original case by default
}

-- Enhanced string normalization with comprehensive character mapping
---@param str string Input string
---@param options NormalizeOptions? Normalization options
---@return string normalized Normalized string
function M.normalize_string(str, options)
    if not str then
        return ""
    end

    options = vim.tbl_deep_extend("force", DEFAULT_NORMALIZE_OPTIONS, options or {})
    local normalized = str

    -- Replace smart quotes
    if options.smart_quotes then
        for smart_char, regular_char in pairs(NORMALIZATION_MAPS.SMART_QUOTES) do
            normalized = normalized:gsub(vim.pesc(smart_char), regular_char)
        end
    end

    -- Replace typographic characters
    if options.typographic_chars then
        for typo_char, regular_char in pairs(NORMALIZATION_MAPS.TYPOGRAPHIC) do
            normalized = normalized:gsub(vim.pesc(typo_char), regular_char)
        end
    end

    -- Unescape HTML entities
    if options.html_entities then
        for entity, char in pairs(NORMALIZATION_MAPS.HTML_ENTITIES) do
            normalized = normalized:gsub(vim.pesc(entity), char)
        end
    end

    -- Normalize whitespace
    if options.extra_whitespace then
        normalized = normalized:gsub("%s+", " ")
    end

    -- Normalize case
    if options.normalize_case then
        normalized = normalized:lower()
    end

    -- Trim whitespace
    if options.trim then
        normalized = normalized:gsub("^%s+", ""):gsub("%s+$", "")
    end

    return normalized
end

-- Quick normalization for common code comparison scenarios
---@param str string Input string
---@return string normalized Normalized string optimized for code comparison
function M.normalize_for_code(str)
    return M.normalize_string(str, {
        smart_quotes = true,
        typographic_chars = true,
        html_entities = true,
        extra_whitespace = true,
        trim = true,
        normalize_case = false, -- Preserve case for code
    })
end

-- Aggressive normalization for fuzzy matching
---@param str string Input string
---@return string normalized Heavily normalized string for fuzzy matching
function M.normalize_aggressive(str)
    return M.normalize_string(str, {
        smart_quotes = true,
        typographic_chars = true,
        html_entities = true,
        extra_whitespace = true,
        trim = true,
        normalize_case = true, -- Ignore case for fuzzy matching
    })
end

-- Normalize punctuation for flexible matching
---@param line string Input line
---@return string normalized Line with normalized punctuation
function M.normalize_punctuation(line)
    line = line
        -- Remove trailing commas and semicolons
        :gsub(",%s*$", "")
        :gsub(";%s*$", "")
        -- Normalize comma and semicolon spacing
        :gsub("%s*,%s*", ", ")
        :gsub("%s*;%s*", "; ")
        -- Normalize parentheses spacing
        :gsub("%s*%(%s*", "(")
        :gsub("%s*%)%s*", ")")
        -- Normalize bracket spacing
        :gsub("%s*%[%s*", "[")
        :gsub("%s*%]%s*", "]")
        -- Normalize brace spacing
        :gsub("%s*{%s*", "{")
        :gsub("%s*}%s*", "}")
    return line
end

-- Levenshtein distance implementation for accurate string similarity
---@param str1 string First string
---@param str2 string Second string
---@return integer distance Edit distance between strings
function M.levenshtein_distance(str1, str2)
    local len1, len2 = #str1, #str2

    -- Handle edge cases
    if len1 == 0 then
        return len2
    end
    if len2 == 0 then
        return len1
    end
    if str1 == str2 then
        return 0
    end

    -- Initialize matrix
    local matrix = {}
    for i = 0, len1 do
        matrix[i] = { [0] = i } -- Cost to delete i characters
    end
    for j = 0, len2 do
        matrix[0][j] = j -- Cost to insert j characters
    end

    -- Fill matrix using dynamic programming
    for i = 1, len1 do
        for j = 1, len2 do
            local cost = str1:sub(i, i) == str2:sub(j, j) and 0 or 1

            matrix[i][j] = math.min(
                matrix[i - 1][j] + 1, -- DELETE: remove char from str1
                matrix[i][j - 1] + 1, -- INSERT: add char to str1
                matrix[i - 1][j - 1] + cost -- SUBSTITUTE: replace char (or keep if same)
            )
        end
    end

    return matrix[len1][len2]
end

-- Calculate similarity score between two strings (0.0 to 1.0)
---@param str1 string First string
---@param str2 string Second string
---@return number similarity Similarity score (1.0 = identical, 0.0 = completely different)
function M.calculate_similarity(str1, str2)
    if str1 == str2 then
        return 1.0
    end

    local distance = M.levenshtein_distance(str1, str2)
    local max_length = math.max(#str1, #str2)

    return max_length > 0 and (1 - distance / max_length) or 0
end

local types = require("mcphub.native.neovim.files.edit_file.types")

-- Advanced line comparison with multiple normalization levels
---@param line1 string First line
---@param line2 string Second line
---@return LineMatchType match_type Type of match found
---@return number score Similarity score
---@return DifferenceType[]? differences List of difference types found
function M.compare_lines(line1, line2)
    -- Level 1: Exact match
    if line1 == line2 then
        return types.LINE_MATCH_TYPE.exact, 1.0, {}
    end

    -- Level 2: Whitespace-only normalization (common case)
    local ws_norm1 = vim.trim(line1):gsub("%s+", " ")
    local ws_norm2 = vim.trim(line2):gsub("%s+", " ")
    if ws_norm1 == ws_norm2 then
        return types.LINE_MATCH_TYPE.exact_whitespace, 0.99, { types.DIFFERENCE_TYPE.whitespace }
    end

    -- Level 3: Code normalization (smart quotes, HTML entities, whitespace)
    local code_norm1 = M.normalize_for_code(line1)
    local code_norm2 = M.normalize_for_code(line2)
    if code_norm1 == code_norm2 then
        local differences = M._detect_differences(line1, line2)
        return types.LINE_MATCH_TYPE.normalized, 0.98, differences
    end

    -- Level 4: Punctuation flexible match (common formatter changes)
    local punct1 = M.normalize_punctuation(code_norm1)
    local punct2 = M.normalize_punctuation(code_norm2)
    if punct1 == punct2 then
        local differences = M._detect_differences(line1, line2)
        table.insert(differences, types.DIFFERENCE_TYPE.punctuation)
        return types.LINE_MATCH_TYPE.punctuation, 0.95, differences
    end

    -- Level 5: Case-insensitive comparison (aggressive normalization)
    local aggr1 = M.normalize_aggressive(line1)
    local aggr2 = M.normalize_aggressive(line2)
    if aggr1 == aggr2 then
        local differences = M._detect_differences(line1, line2)
        table.insert(differences, types.DIFFERENCE_TYPE.case)
        return types.LINE_MATCH_TYPE.case_insensitive, 0.90, differences
    end

    -- Level 6: Fuzzy similarity using Levenshtein on normalized strings
    local similarity = M.calculate_similarity(aggr1, aggr2)
    local differences = M._detect_differences(line1, line2)

    if similarity >= 0.85 then
        return types.LINE_MATCH_TYPE.fuzzy_high, similarity, differences
    elseif similarity >= 0.70 then
        return types.LINE_MATCH_TYPE.fuzzy_medium, similarity, differences
    elseif similarity >= 0.50 then
        return types.LINE_MATCH_TYPE.fuzzy_low, similarity, differences
    else
        return types.LINE_MATCH_TYPE.no_match, similarity, differences
    end
end

-- Detect what types of differences exist between two lines
---@param line1 string First line
---@param line2 string Second line
---@return DifferenceType[] differences List of difference types
function M._detect_differences(line1, line2)
    local differences = {}

    -- Check for quote style differences
    if line1:gsub("'", '"') == line2:gsub("'", '"') or line1:gsub('"', "'") == line2:gsub('"', "'") then
        table.insert(differences, types.DIFFERENCE_TYPE.quote_style)
    end

    -- Check for whitespace differences
    if vim.trim(line1):gsub("%s+", " ") == vim.trim(line2):gsub("%s+", " ") then
        table.insert(differences, types.DIFFERENCE_TYPE.whitespace)
    end

    -- Check for case differences
    if line1:lower() == line2:lower() then
        table.insert(differences, types.DIFFERENCE_TYPE.case)
    end

    -- Check for HTML entity differences
    local html_norm1 =
        M.normalize_string(line1, { html_entities = true, smart_quotes = false, typographic_chars = false })
    local html_norm2 =
        M.normalize_string(line2, { html_entities = true, smart_quotes = false, typographic_chars = false })
    if html_norm1 == html_norm2 and line1 ~= line2 then
        table.insert(differences, types.DIFFERENCE_TYPE.html_entities)
    end

    return differences
end

return M

--[[ MCPHub highlight utilities ]]
local M = {}
M.setup_called = false

-- Highlight group names
M.groups = {
    title = "MCPHubTitle",
    header = "MCPHubHeader",
    header_btn = "MCPHubHeaderBtn",
    header_btn_shortcut = "MCPHubHeaderBtnShortcut",
    header_shortcut = "MCPHubHeaderShortcut",
    keymap = "MCPHubKeymap",

    success = "MCPHubSuccess",
    success_italic = "MCPHubSuccessItalic",
    success_fill = "MCPHubSuccessFill",
    info = "MCPHubInfo",
    warn = "MCPHubWarning",
    warn_fill = "MCPHubWarnFill",
    warn_italic = "MCPHubWarnItalic",
    error = "MCPHubError",
    error_fill = "MCPHubErrorFill",
    muted = "MCPHubMuted",
    link = "MCPHubLink",

    -- Button highlights for confirmation dialogs
    button_active = "MCPHubButtonActive",
    button_inactive = "MCPHubButtonInactive",
    -- Seamless border (matches float background)
    seamless_border = "MCPHubSeamlessBorder",

    -- JSON syntax highlights
    json_property = "MCPHubJsonProperty",
    json_string = "MCPHubJsonString",
    json_number = "MCPHubJsonNumber",
    json_boolean = "MCPHubJsonBoolean",
    json_null = "MCPHubJsonNull",
    json_punctuation = "MCPHubJsonPunctuation",
    -- Markdown specific highlights
    text = "MCPHubText", -- Regular markdown text
    code = "MCPHubCode", -- Code blocks
    heading = "MCPHubHeading", -- Markdown headings
    -- Diff visualization highlights
    diff_add = "MCPHubDiffAdd", -- New content being added
    diff_change = "MCPHubDiffChange", -- New content being added
    diff_delete = "MCPHubDiffDelete", -- Content being removed
}

-- Get highlight attributes from a highlight group
local function get_hl_attrs(name)
    local ok, hl = pcall(vim.api.nvim_get_hl, 0, { name = name })
    if not ok or not hl then
        return {}
    end
    return hl
end

-- Get color from highlight group or fallback
local function get_color(group, attr, fallback)
    local hl = get_hl_attrs(group)
    local val = hl[attr] and string.format("#%06x", hl[attr])
    if not val then
        val = fallback
    end
    return val
end

-- Apply highlight groups
function M.apply_highlights()
    -- Get colors from current theme
    local normal_bg = get_color("Normal", "bg", "#1a1b26")
    local normal_fg = get_color("Normal", "fg", "#c0caf5")
    local float_bg = get_color("NormalFloat", "bg", normal_bg)
    local border_fg = get_color("FloatBorder", "fg", "#555555")
    local border_bg = get_color("FloatBorder", "bg", normal_bg)
    local comment_fg = get_color("Comment", "fg", "#808080")

    -- Get semantic colors
    local error_color = get_color("DiagnosticError", "fg", "#f44747")
    local warn_color = get_color("DiagnosticWarn", "fg", "#ff8800")
    local info_color = get_color("DiagnosticInfo", "fg", "#4fc1ff")
    local success_color = get_color("DiagnosticHint", "fg", "#89d185")
    local macro_color = get_color("Macro", "fg", "#98c379")

    -- Get UI colors
    local pmenu_sel_bg = get_color("PmenuSel", "bg", "#444444")
    local pmenu_sel_fg = get_color("PmenuSel", "fg", "#d4d4d4")
    local special_key = get_color("Special", "fg", "#ff966c")
    local title_color = get_color("Title", "fg", "#c792ea")

    local highlights = {
        -- Title and headers
        -- pink
        [M.groups.title] = "Title",

        -- Header buttons - these need custom styling for visibility
        [M.groups.header_btn] = {
            fg = normal_bg,
            bg = title_color,
            bold = true,
        },
        [M.groups.header_btn_shortcut] = {
            fg = normal_bg,
            bg = title_color,
            bold = true,
        },

        -- Header components - use existing UI highlights for consistency
        [M.groups.header] = {
            fg = title_color,
            bg = "NONE",
            bold = true,
        },
        [M.groups.header_shortcut] = {
            fg = title_color,
            bold = true,
        },

        -- Button highlights for confirmation dialogs
        [M.groups.button_active] = "Visual",
        [M.groups.button_inactive] = "CursorLine",
        -- Status and messages
        [M.groups.error] = {
            bg = "NONE",
            fg = error_color,
        },
        [M.groups.error_fill] = {
            bg = error_color,
            fg = normal_bg,
            bold = true,
        },
        [M.groups.warn] = {
            bg = "NONE",
            fg = warn_color,
        },
        [M.groups.warn] = {
            bg = "NONE",
            fg = warn_color,
        },
        [M.groups.warn_italic] = {
            bg = "NONE",
            fg = warn_color,
            italic = true,
        },
        [M.groups.warn_fill] = {
            bg = warn_color,
            fg = normal_bg,
            bold = true,
        },
        [M.groups.info] = {
            bg = "NONE",
            fg = info_color,
        },
        [M.groups.success] = {
            bg = "NONE",
            fg = success_color,
        },
        [M.groups.success_italic] = {
            bg = "NONE",
            fg = success_color,
            bold = true,
            italic = true,
        },
        [M.groups.success_fill] = {
            bg = success_color,
            fg = normal_bg,
            bold = true,
        },
        [M.groups.muted] = {
            fg = comment_fg,
        },
        [M.groups.keymap] = {
            fg = special_key,
            bold = true,
        },
        [M.groups.link] = {
            bg = "NONE",
            fg = info_color,
            underline = true,
        },
        -- JSON syntax highlights linked to built-in groups
        [M.groups.json_property] = "@property",
        [M.groups.json_string] = "String",
        [M.groups.json_number] = "Number",
        [M.groups.json_boolean] = "Boolean",
        [M.groups.json_null] = "keyword",
        [M.groups.json_punctuation] = "Delimiter",

        -- Markdown highlights
        [M.groups.text] = {
            fg = normal_fg,
            bg = "NONE",
        },
        [M.groups.code] = "Special",
        [M.groups.heading] = "Title",

        -- Seamless border (matches float background)
        [M.groups.seamless_border] = "FloatBorder",

        -- Diff visualization highlights
        -- [M.groups.diff_add] = "DiffAdd",
        -- [M.groups.diff_change] = "DiffChange",
        -- [M.groups.diff_delete] = "DiffDelete",
        -- Add shaded background for diff highlights
        [M.groups.diff_add] = {
            bg = "#1a2b32",
            fg = "#1abc9c",
            bold = true,
        },
        [M.groups.diff_change] = "DiffChange",
        [M.groups.diff_delete] = {
            bg = "#2d202a",
            fg = "#db4b4b",
            italic = true,
        },
    }

    for group, link in pairs(highlights) do
        local hl = type(link) == "table" and link or { link = link }
        hl.default = true
        vim.api.nvim_set_hl(0, group, hl)
    end
end

function M.setup()
    if M.setup_called then
        return
    end
    M.setup_called = true
    M.apply_highlights()

    local group = vim.api.nvim_create_augroup("MCPHubHighlights", { clear = true })
    vim.api.nvim_create_autocmd("ColorScheme", {
        group = group,
        callback = M.apply_highlights,
        desc = "Update MCPHub highlights when colorscheme changes",
    })
    vim.api.nvim_create_autocmd("VimEnter", {
        group = group,
        callback = M.apply_highlights,
        desc = "Apply MCPHub highlights on VimEnter",
    })
end

return M

local M = {}

local uv = (vim.loop or vim.uv)
local path = require("plenary.path")
local utils = require("mcphub.utils")

---Search upward from current directory for any of the look_for files
---@param look_for_patterns string[] Array of file patterns to look for
---@param start_dir? string Starting directory (defaults to current working directory)
---@return table|nil { root_dir = "/path", config_file = "/path/.vscode/mcp.json" } or nil
function M.find_workspace_config(look_for_patterns, start_dir)
    start_dir = start_dir or vim.fn.getcwd()

    local current_dir = path:new(start_dir)
    local parent_dir = current_dir:parent()
    -- Search upward until we hit the root
    while current_dir and tostring(current_dir) ~= tostring(parent_dir) do
        -- Check each pattern in order
        for _, pattern in ipairs(look_for_patterns) do
            local config_file = current_dir / pattern
            if config_file:exists() then
                return {
                    root_dir = tostring(current_dir),
                    config_file = tostring(config_file),
                }
            end
        end

        -- Move up one directory
        current_dir = parent_dir
        parent_dir = current_dir:parent()
    end

    return nil
end

---Hash workspace path to generate a consistent port
---@param workspace_path string Absolute path to workspace root
---@param port_range table {min: number, max: number}
---@return number Generated port number
function M.generate_workspace_port(workspace_path, port_range)
    -- Simple hash function using string bytes
    local hash = 0
    for i = 1, #workspace_path do
        hash = (hash * 31 + string.byte(workspace_path, i)) % 2147483647
    end

    -- Map hash to port range
    local range_size = port_range.max - port_range.min + 1
    return port_range.min + (hash % range_size)
end

---Check if a port is available
---@param port number Port number to check
---@return boolean True if port is available
function M.is_port_available(port)
    local handle = uv.new_tcp()
    if not handle then
        return false
    end

    local success = handle:bind("127.0.0.1", port)
    handle:close()

    return success
end

---Find next available port starting from generated port
---@param workspace_path string Workspace root path
---@param port_range table {min: number, max: number}
---@param max_attempts? number Maximum attempts to find a port (default: 100)
---@return number|nil Available port number or nil if none found
function M.find_available_port(workspace_path, port_range, max_attempts)
    max_attempts = max_attempts or 100

    local base_port = M.generate_workspace_port(workspace_path, port_range)

    for i = 0, max_attempts - 1 do
        local port = base_port + i

        -- Wrap around if we exceed max port
        if port > port_range.max then
            port = port_range.min + (port - port_range.max - 1)
        end

        if M.is_port_available(port) then
            return port
        end
    end

    return nil
end

---Get the path to the global workspace cache file
---@return string Absolute path to workspace cache file
function M.get_workspace_cache_path()
    -- Get home directory using Neovim's cross-platform approach
    local home = vim.fn.expand("~")

    if not home or home == "~" then
        -- Fallback to environment variables if expand fails
        home = os.getenv("HOME")
            or os.getenv("USERPROFILE")
            or (os.getenv("HOMEDRIVE") and os.getenv("HOMEPATH") and (os.getenv("HOMEDRIVE") .. os.getenv("HOMEPATH")))
    end

    if not home then
        error("Unable to determine user home directory")
    end

    -- Check for legacy path first (same logic as backend)
    local legacy_path = vim.fs.joinpath(home, ".mcp-hub", "workspaces.json")

    -- Use legacy path if it exists (matching backend behavior)
    if vim.fn.filereadable(legacy_path) == 1 then
        return legacy_path
    end

    -- Otherwise use XDG-compliant path
    local state_home = os.getenv("XDG_STATE_HOME") or vim.fs.joinpath(home, ".local", "state")
    local cache_dir = vim.fs.joinpath(state_home, "mcp-hub")
    return vim.fs.joinpath(cache_dir, "workspaces.json")
end

---Read the global workspace cache from mcp-hub's state directory
---@return table Table of workspace_path -> {pid, port, startTime}
function M.read_workspace_cache()
    local cache_path = M.get_workspace_cache_path()
    local cache_file = path:new(cache_path)

    if not cache_file:exists() then
        return {}
    end

    local content = cache_file:read()
    if not content or content == "" then
        return {}
    end

    local success, cache = utils.json_decode(content)
    if not success then
        vim.notify("Failed to parse workspace cache: " .. cache_path, vim.log.levels.WARN)
        return {}
    end

    return cache or {}
end

---Check if a process is still running
---@param pid number Process ID to check
---@return boolean True if process is running
function M.is_process_running(pid)
    if not pid or type(pid) ~= "number" then
        return false
    end

    -- Use kill with signal 0 to check if process exists
    local success = pcall(function()
        uv.kill(pid, 0)
    end)

    return success
end

---Get active hub info for a workspace
---@param port string current port of the hub
---@return table|nil {port, pid, startTime, config_files} or nil if no active hub
function M.get_workspace_hub_info(port)
    port = tostring(port) -- Ensure port is a string
    local cache = M.read_workspace_cache()
    local entry = cache[port]

    if not entry then
        return nil
    end

    -- Check if the process is still running
    if not M.is_process_running(entry.pid) then
        return nil
    end

    return entry
end

-- Enhanced function to find matching hub by workspace + config
function M.find_matching_workspace_hub(workspace_path, config_files)
    local cache = M.read_workspace_cache()

    -- Search through ALL cache entries (keyed by port now)
    for port_str, entry in pairs(cache) do
        -- Check if workspace matches
        if entry.cwd == workspace_path then
            -- Check if config files match
            if vim.deep_equal(entry.config_files or {}, config_files) then
                -- Check if process is still running
                if M.is_process_running(entry.pid) then
                    return {
                        port = entry.port,
                        pid = entry.pid,
                        startTime = entry.startTime,
                        config_files = entry.config_files,
                        workspace_path = entry.cwd,
                    }
                end
            end
        end
    end

    return nil
end

---Check if workspace mode is enabled
---@return boolean True if workspace mode is enabled
function M.is_workspace_enabled()
    local State = require("mcphub.state")
    return State.config and State.config.workspace and State.config.workspace.enabled == true
end

return M

local State = require("mcphub.state")
---New modular editor tool using EditSession
---@type MCPTool
local edit_file_tool = {
    name = "edit_file",
    description = [[Replace multiple sections in a file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool starts an interactive edit session in Neovim. The user might accept some changes, reject some or add new text during the edit session. Once the edit session completes the result will include useful information like diff and feedback which you MUST take into account for SUBSEQUENT conversation:
1. A diff comparing the file before and after the edit session. The diff might be a result of a combination of:
   - Changes from successfully applied SEARCH/REPLACE blocks
   - Changes made by the USER during the edit session
   - Changes made by the FORMATTERS or LINTERS that were run before the file is saved
2. Feedback from the edit session, which might include:
   - Any issues while PARSING the SEARCH/REPLACE blocks and how they were resolved
   - Any issues encountered while FINDING the SEARCH content in the file like:
     - SEARCH content not found (will provide the best match found for the SEARCH content) or
     - SEARCH content found but with fuzzy matching (will provide a confidence score and the diff between SEARCH content and the fuzzy match)
   - Any additional user feedback provided during the edit session
3. Diagnostics in the file after the edit session is completed

IMPORTANT: The diff will show you what all changes were made, and the feedback will provide additional context on how the SEARCH/REPLACE blocks were applied to avoid any issues in subsequent calls. You MUST give EXTREME care to the result of this tool or else you will be fired!!!
IMPORTANT: The tool is NEVER wrong. Once edits are shown in the buffer, user might make any additional changes like adding some new comment or editing the replace text you sent. This MUST be considered as intentional and is not a bug in the tool. Hence, careful observation of the diff and feedback is CRITICAL to avoid any issues in subsequent calls.
]],
    needs_confirmation_window = false, -- will show interactive diff, avoid double confirmations
    inputSchema = {
        type = "object",
        properties = {
            path = {
                type = "string",
                description = "The path to the file to modify",
            },
            diff = {
                type = "string",
                description = [[One or more SEARCH/REPLACE blocks following this exact format:

<<<<<<< SEARCH
[exact content to find]
=======
[new content to replace with]
>>>>>>> REPLACE

CRITICAL:
- When there are two or more related changes needed in a file, always use multiple SEARCH/REPLACE blocks in the diff from the start of the file to the end. Each block should contain the exact content to find and the new content to replace it with. Failing to do so or using multiple calls with single SEARCH/REPLACE block will result in you being fired!!!
- The markers `<<<<<<< SEARCH`, `=======`, and `>>>>>>> REPLACE` MUST be exact with no other characters on the line.

Examples:

1. Multiple changes in one call from top to bottom:
<<<<<<< SEARCH
import os
=======
import os
import json
>>>>>>> REPLACE

<<<<<<< SEARCH
def process_data():
    # old implementation
    pass
=======
def process_data():
    # new implementation
    with open('data.json') as f:
        return json.load(f)
>>>>>>> REPLACE

<<<<<<< SEARCH
if __name__ == '__main__':
    print("Starting")
=======
if __name__ == '__main__':
    print("Starting with new config")
    process_data()
>>>>>>> REPLACE

2. Deletion example:
<<<<<<< SEARCH
def unused_function():
    return "delete me"

=======
>>>>>>> REPLACE

3. Adding new content at end:
CAUTION: Whitespaces or newlines without any other content in the SEARCH section will replace the entire file!!! This will lead to loss of all content in the file. Searching for empty lines or whitespace in order to replace something is not allowed. Only use empty SEARCH blocks if you want to replace the ENTIRE file content.
<<<<<<< SEARCH
    return result

=======
    return result

def new_helper_function():
    return "helper"
>>>>>>> REPLACE

4. Replacing same content multiple times:
<<<<<<< SEARCH
count = 0
=======
counter = 0
>>>>>>> REPLACE

<<<<<<< SEARCH
print("Count is", count)
=======
print("Counter is", counter)
>>>>>>> REPLACE

<<<<<<< SEARCH
print("Count is", count)
=======
print("Counter is", counter)
>>>>>>> REPLACE

CRITICAL RULE:
When the SEARCH or REPLACE content includes lines that start with markers like `<<<<<<<`, `=======`, or `>>>>>>>`, you MUST escape them by adding a backslash before each marker so that tool doesn't parse them as actual markers. For example, to search for content that has `<<<<<<< SEARCH`, use `\<<<<<<< SEARCH` in the SEARCH block.

5. Escaping markers in SEARCH/REPLACE content:
<<<<<<< SEARCH
Tutorial:
A marker has < or > or = in it. E.g
\<<<<<<< SEARCH
=======
Tutorial:
A marker will have < or > or = in it. e.g
\=======
>>>>>>> REPLACE

CRITICAL rules:
1. SEARCH content must match the file section EXACTLY:
   - Character-for-character including whitespace, indentation, line endings
   - Include all comments, docstrings, etc.
2. SEARCH/REPLACE blocks will ONLY replace the first match occurrence
   - To replace same content multiple times: Use multiple SEARCH/REPLACE blocks for each occurrence
   - When using multiple SEARCH/REPLACE blocks, list them in the order they appear in the file
3. Keep SEARCH/REPLACE blocks concise:
   - Include just the changing lines, and a few surrounding lines if needed for uniqueness
   - Break large blocks into smaller blocks that each change a small portion. Searching for entire functions or large sections when only a few lines need changing will get you fired!!!
   - Each line must be complete. Never truncate lines mid-way through as this can cause matching failures
4. Special operations:
   - To move code: Use two blocks (one to delete from original + one to insert at new location)
   - To delete code: Use empty REPLACE section

IMPORTANT: Batch multiple related changes for a file into a single call to minimize user interactions.
]],
            },
        },
        required = { "path", "diff" },
    },
    handler = function(req, res)
        local params = req.params
        if not params.path or vim.trim(params.path) == "" then
            return res:error("Missing required parameter: path")
        end
        if not params.diff or vim.trim(params.diff) == "" then
            return res:error("Missing required parameter: diff")
        end
        -- Handle hub UI cleanup
        if req.caller and req.caller.type == "hubui" then
            req.caller.hubui:cleanup()
        end
        local EditSession = require("mcphub.native.neovim.files.edit_file.edit_session")
        local session = EditSession.new(params.path, params.diff, State.config.builtin_tools.edit_file)
        session:start({
            interactive = req.caller.auto_approve ~= true,
            on_success = function(summary)
                res:text(summary):send()
            end,
            on_error = function(error_report)
                res:error(error_report)
            end,
        })
    end,
}

return edit_file_tool

-- Issue Tracker for Editor Operations
-- Tracks parsing and processing issues and provides feedback to LLMs

local M = {}

-- Issue types that can be tracked and reported back to LLM
M.ISSUE_TYPES = {
    EXTRA_SPACES_IN_MARKERS = {
        description = "Extra spaces found in search/replace markers",
        fix = "Normalized marker spacing",
        llm_guidance = "Use exactly one space: '<<<<<<< SEARCH' not '<<<<<<<  SEARCH'",
        severity = "warning",
    },

    CASE_MISMATCH_MARKERS = {
        description = "Inconsistent case in SEARCH/REPLACE keywords",
        fix = "Converted to uppercase",
        llm_guidance = "Always use uppercase: 'SEARCH' and 'REPLACE'",
        severity = "warning",
    },

    MISSING_SPACES_IN_MARKERS = {
        description = "No space between markers and keywords",
        fix = "Added missing space",
        llm_guidance = "Use space: '<<<<<<< SEARCH' not '<<<<<<<SEARCH'",
        severity = "warning",
    },

    MARKDOWN_NOISE = {
        description = "Markdown code blocks around diff content",
        fix = "Stripped markdown formatting",
        llm_guidance = "Don't wrap diff blocks in markdown code blocks",
        severity = "info",
    },

    MALFORMED_SEARCH_MARKER = {
        description = "Missing or malformed SEARCH marker",
        fix = "Added missing SEARCH marker",
        llm_guidance = "Always start blocks with '<<<<<<< SEARCH'",
        severity = "error",
    },

    FUZZY_MATCH_WARNING = {
        description = "Block found with fuzzy matching instead of exact match",
        fix = "Applied fuzzy matching with confidence score",
        llm_guidance = "Consider using exact content from the file to avoid fuzzy matching",
        severity = "info",
    },

    BLOCK_LOCATION_FAILED = {
        description = "Failed to locate block content in file",
        fix = "Block skipped",
        llm_guidance = "Ensure the search content exists exactly in the target file",
        severity = "error",
    },
    CONTENT_ON_MARKER_LINE = function(details)
        return {
            description = string.format("`<<<<<<< SEARCH` marker line contains content: `%s`", details.inline_content),
            fix = "Removed content on the marker line and used it as first line in the SEARCH block.",
            llm_guidance = "`<<<<<<< SEARCH` marker line should not contain any content, only the marker itself",
            severity = "warning",
        }
    end,
    CLAUDE_MARKER_ISSUE = function(details)
        local inline_content = details.inline_content
        local line = details.line
        return {
            description = string.format("`<<<<<<< SEARCH` marker line is not EXACT. `%s` found instead", line),
            fix = "Removed `>`"
                .. (
                    inline_content ~= "" and " and content on that line is used as first line in the SEARCH block."
                    or ""
                ),
            llm_guidance = "`<<<<<<< SEARCH` marker line should be EXACT without any other characters like `>` or other content on the marker lines",
            severity = "warning",
        }
    end,
}

---@class IssueTracker
---@field issues_found ParsingIssue[] List of tracked issues
local IssueTracker = {}
IssueTracker.__index = IssueTracker

-- Create a new issue tracker instance
---@return IssueTracker
function M.new()
    local self = setmetatable({}, IssueTracker)
    self.issues_found = {}
    return self
end

-- Track a new issue
---@param issue_type string Issue type from ISSUE_TYPES
---@param details table? Issue-specific details
function IssueTracker:track_issue(issue_type, details)
    if not M.ISSUE_TYPES[issue_type] then
        error("Unknown issue type: " .. tostring(issue_type))
    end
    local issue_info = M.ISSUE_TYPES[issue_type]
    if type(issue_info) == "function" then
        issue_info = issue_info(details)
    end
    if issue_info then
        table.insert(self.issues_found, {
            type = issue_type,
            details = details or {},
            timestamp = os.time(),
            severity = issue_info.severity,
            description = issue_info.description,
            fix = issue_info.fix,
            llm_guidance = issue_info.llm_guidance,
        })
    end
end

-- Check if any issues were found
---@return boolean has_issues
function IssueTracker:has_issues()
    return #self.issues_found > 0
end

-- Generate LLM feedback with categorized issues
---@return string? feedback Formatted feedback for LLM (nil if no issues)
function IssueTracker:get_llm_feedback()
    if #self.issues_found == 0 then
        return nil
    end

    local parts = {}

    for _, issue in ipairs(self.issues_found) do
        local str = string.format(
            "### %s\nIssue Encountered: %s\nResolved By Editor: %s\nFuture Guidance: %s",
            issue.severity:upper(),
            issue.description,
            issue.fix,
            issue.llm_guidance
        )
        table.insert(parts, str)
    end
    return #parts > 0 and table.concat(parts, "\n\n") or nil
end

-- Clear all tracked issues
function IssueTracker:clear()
    self.issues_found = {}
end

return M

local M = {}

--- @class LogConfig
--- @field level number Default log level
--- @field to_file boolean Whether to log to file
--- @field file_path? string Path to log file
--- @field prefix string Prefix for log messages
local config = {
    level = vim.log.levels.ERROR,
    to_file = false,
    file_path = nil,
    prefix = "MCPHub",
}

--- Setup logger configuration
--- @param opts LogConfig
function M.setup(opts)
    config = vim.tbl_deep_extend("force", config, opts or {})

    -- Create log directory if logging to file
    if config.to_file and config.file_path then
        local path = vim.fn.fnamemodify(config.file_path, ":h")
        vim.fn.mkdir(path, "p")
    end
end

--- Format structured message
--- @param msg string|table Message or structured data
--- @param level_str string Level string for log prefix
--- @return string formatted_message
local function format_message(msg, level_str)
    local str = ""
    if type(msg) == "table" then
        -- Handle structured logs (from server)
        if msg.code and msg.message then
            local base = string.format("[%s] %s", msg.code, msg.message)
            if msg.data then
                str = string.format("%s\nData: %s", base, vim.inspect(msg.data))
            else
                str = base
            end
        end
        -- Regular table data
        str = vim.inspect(msg)
    else
        str = msg
    end
    return str
end

--- Write to log file
--- @param formatted string Formatted message
--- @param level_str string Level string
--- @param level number Log level
--- @return boolean success Whether the write was successful
local function write_to_file(formatted, level_str, level)
    -- Only write if:
    -- 1. File logging is enabled and path is set
    -- 2. Level meets minimum configured level
    if not (config.to_file and config.file_path) or level < config.level then
        return false
    end

    local timestamp = os.date("%H:%M:%S")
    local log_line = string.format("(%s) %s [%s] %s\n", vim.fn.getpid(), timestamp, level_str, formatted)

    local f = io.open(config.file_path, "a")
    if f then
        f:write(log_line)
        f:close()
        return true
    end
    return false
end

--- Internal logging function
--- @param msg string|table Message or structured data
--- @param level number Log level
local function log_internal(msg, level)
    -- Early return if below configured level and not an error
    if level < config.level and level < vim.log.levels.ERROR then
        return
    end

    local level_str = ({
        [vim.log.levels.DEBUG] = "debug",
        [vim.log.levels.INFO] = "info",
        [vim.log.levels.WARN] = "warn",
        [vim.log.levels.ERROR] = "error",
        [vim.log.levels.TRACE] = "trace",
    })[level] or "unknown"

    local formatted = format_message(msg, level_str:upper())
    local wrote_to_file = write_to_file(formatted, level_str:upper(), level)

    -- Only notify if:
    -- 1. It's an error (always show errors) OR
    -- 2. Level meets minimum AND we didn't write to file
    if level >= vim.log.levels.ERROR or (level >= config.level and not wrote_to_file) then
        vim.schedule(function()
            vim.notify(formatted, level)
        end)
    end
end

--- Log a debug message
--- @param msg string|table
function M.debug(msg)
    log_internal(msg, vim.log.levels.DEBUG)
end

--- Log an info message
--- @param msg string|table
function M.info(msg)
    log_internal(msg, vim.log.levels.INFO)
end

--- Log a warning message
--- @param msg string|table
function M.warn(msg)
    log_internal(msg, vim.log.levels.WARN)
end

--- Log an error message
--- @param msg string|table
function M.error(msg)
    log_internal(msg, vim.log.levels.ERROR)
end

function M.trace(msg)
    log_internal(msg, vim.log.levels.TRACE)
end

--- Log with call stack information
--- @param msg string|table Message to log
--- @param level? number Log level (default: DEBUG)
function M.stack(msg, level)
    level = level or vim.log.levels.DEBUG

    -- Get call stack starting from level 3 to skip this function and log_internal
    local stack = {}
    local stack_level = 3

    while true do
        local info = debug.getinfo(stack_level, "Snl")
        if not info then
            break
        end

        local location = info.short_src .. ":" .. (info.currentline or "?")
        local func_name = info.name or "<anonymous>"

        table.insert(stack, string.format("  %s in %s", location, func_name))
        stack_level = stack_level + 1

        -- Limit stack depth to prevent spam
        if #stack >= 10 then
            table.insert(stack, "  ...")
            break
        end
    end

    local formatted_msg = msg
    if #stack > 0 then
        formatted_msg = msg .. "\nCall stack:\n" .. table.concat(stack, "\n")
    end

    log_internal(formatted_msg, level)
end

return M

local lsp_utils = require("mcphub.native.neovim.utils.lsp")
local mcphub = require("mcphub")

mcphub.add_resource("neovim", {
    name = "Diagnostics: Buffer",
    description = "Get diagnostics for the current file",
    uri = "neovim://diagnostics/buffer",
    mimeType = "text/plain",
    handler = function(req, res)
        -- local context = utils.parse_context(req.caller)
        local buf_info = req.editor_info.last_active
        if not buf_info then
            return res:error("No active buffer found")
        end
        local bufnr = buf_info.bufnr
        local filepath = buf_info.filename
        local diagnostics = vim.diagnostic.get(bufnr)
        local text = string.format("Diagnostics for: %s\n%s\n", filepath, string.rep("-", 40))
        for _, diag in ipairs(diagnostics) do
            local severity = vim.diagnostic.severity[diag.severity] or "UNKNOWN"
            local line_str = string.format("Line %d, Col %d", diag.lnum + 1, diag.col + 1)
            local source = diag.source and string.format("[%s]", diag.source) or ""
            local code = diag.code and string.format(" (%s)", diag.code) or ""

            -- Get range information if available
            local range_info = ""
            if diag.end_lnum and diag.end_col then
                range_info = string.format(" to Line %d, Col %d", diag.end_lnum + 1, diag.end_col + 1)
            end

            text = text
                .. string.format(
                    "\n%s: %s\n  Location: %s%s\n  Message: %s%s\n",
                    severity,
                    source,
                    line_str,
                    range_info,
                    diag.message,
                    code
                )

            text = text .. string.rep("-", 40) .. "\n"
        end
        return res:text(text ~= "" and text or "No diagnostics found"):send()
    end,
})

mcphub.add_resource("neovim", {
    name = "Diagnostics: Workspace",
    description = "Get diagnostics for all open buffers",
    uri = "neovim://diagnostics/workspace",
    mimeType = "text/plain",
    handler = function(req, res)
        local diagnostics = lsp_utils.get_all_diagnostics()
        local text = "Workspace Diagnostics\n" .. string.rep("=", 40) .. "\n\n"

        -- Group diagnostics by buffer
        local by_buffer = {}
        for _, diag in ipairs(diagnostics) do
            by_buffer[diag.bufnr] = by_buffer[diag.bufnr] or {}
            table.insert(by_buffer[diag.bufnr], diag)
        end

        -- Format diagnostics for each buffer
        for bufnr, diags in pairs(by_buffer) do
            local filename = vim.api.nvim_buf_get_name(bufnr)
            text = text .. string.format("File: %s\n%s\n", filename, string.rep("-", 40))

            for _, diag in ipairs(diags) do
                local severity = vim.diagnostic.severity[diag.severity] or "UNKNOWN"
                local line_str = string.format("Line %d, Col %d", diag.lnum + 1, diag.col + 1)
                local source = diag.source and string.format("[%s]", diag.source) or ""
                local code = diag.code and string.format(" (%s)", diag.code) or ""

                -- Get range information if available
                local range_info = ""
                if diag.end_lnum and diag.end_col then
                    range_info = string.format(" to Line %d, Col %d", diag.end_lnum + 1, diag.end_col + 1)
                end

                text = text
                    .. string.format(
                        "\n%s: %s\n  Location: %s%s\n  Message: %s%s\n",
                        severity,
                        source,
                        line_str,
                        range_info,
                        diag.message,
                        code
                    )
            end
            text = text .. string.rep("-", 40) .. "\n\n"
        end

        return res:text(text ~= "" and text or "No diagnostics found"):send()
    end,
})

local Error = require("mcphub.utils.errors")
local NativeServer = require("mcphub.native.utils.server")
local State = require("mcphub.state")
local validate = require("mcphub.utils.validation")

---@class MCPHub.Native
local Native = {}

--- Check if a server name belongs to a native server
---@param server_name string Name of the server to check
---@return NativeServer|nil server instance if found
function Native.is_native_server(server_name)
    for _, server in ipairs(State.server_state.native_servers) do
        if server.name == server_name then
            return server
        end
    end
    return nil
end

---@param err MCPError Error to handle
local function handle_error(err)
    State:add_error(err)
end

---@class NativeServerDef
---@field name? string Name of the server
---@field displayName? string Display name of the server
---@field capabilities? MCPCapabilities

---@param def NativeServerDef Server definition with name capabilities etc
---@return NativeServer|nil server Server instance or nil on error
function Native.register(def)
    if type(def) ~= "table" then
        handle_error(Error("VALIDATION", Error.Types.NATIVE.INVALID_SCHEMA, "Server definition is not a table"))
        return nil
    end
    local existing = Native.is_native_server(def.name)
    if existing then
        State:add_error(
            Error("VALIDATION", Error.Types.NATIVE.INVALID_SCHEMA, string.format("%s already exists", def.name))
        )
        return existing
    end
    local result = validate.validate_native_server(def)
    if not result.ok then
        handle_error(result.error)
        return nil
    end
    -- Create server instance
    local server = NativeServer:new(def)
    if not server then
        handle_error(Error("RUNTIME", Error.Types.NATIVE.INVALID_SCHEMA, "Failed to create native server"))
        return nil
    end

    -- Update server state with server instance state
    table.insert(State.server_state.native_servers, server)
    return server
end

--- Add a new native server
---@param server_name string Name of the server
---@param def? NativeServerDef Optional server definition overrides
---@return NativeServer|nil server Server instance or nil on error
function Native.add_server(server_name, def)
    -- Check if server already exists
    local existing = Native.is_native_server(server_name)
    if existing then
        State:add_error(
            Error("VALIDATION", Error.Types.NATIVE.INVALID_SCHEMA, string.format("%s already exists", server_name))
        )
        return existing
    end

    -- Create default server definition
    local server_def = vim.tbl_deep_extend("force", {
        name = server_name,
        displayName = server_name,
        capabilities = {
            tools = {},
            resources = {},
            resourceTemplates = {},
            prompts = {},
        },
    }, def or {})
    --make sure the server name is same as the key
    server_def.name = server_name

    -- Register the server
    return Native.register(server_def)
end

--- Add a tool to a server, creating the server if it doesn't exist
---@param server_name string Name of the server
---@param tool_def MCPTool Tool definition including name, description, and handler
---@return NativeServer|nil server The server instance or nil on error
function Native.add_tool(server_name, tool_def)
    local result = validate.validate_tool(tool_def)
    if not result.ok then
        handle_error(result.error)
        return nil
    end

    local server = Native.is_native_server(server_name)
    if server then
        table.insert(server.capabilities.tools, tool_def)
        return server
    else
        return Native.add_server(server_name, {
            name = server_name,
            capabilities = { tools = { tool_def } },
        })
    end
end

--- Add a resource to a server, creating the server if it doesn't exist
---@param server_name string Name of the server
---@param resource_def MCPResource Resource definition including URI and handler
---@return NativeServer|nil server The server instance or nil on error
function Native.add_resource(server_name, resource_def)
    local result = validate.validate_resource(resource_def)
    if not result.ok then
        handle_error(result.error)
        return nil
    end

    local server = Native.is_native_server(server_name)
    if server then
        table.insert(server.capabilities.resources, resource_def)
        return server
    else
        return Native.add_server(server_name, {
            name = server_name,
            capabilities = { resources = { resource_def } },
        })
    end
end

--- Add a resource template to a server, creating the server if it doesn't exist
---@param server_name string Name of the server
---@param template_def MCPResourceTemplate Template definition including URI template and handler
---@return NativeServer|nil server The server instance or nil on error
function Native.add_resource_template(server_name, template_def)
    local result = validate.validate_resource_template(template_def)
    if not result.ok then
        handle_error(result.error)
        return nil
    end

    local server = Native.is_native_server(server_name)
    if server then
        table.insert(server.capabilities.resourceTemplates, template_def)
        return server
    else
        return Native.add_server(server_name, {
            name = server_name,
            capabilities = { resourceTemplates = { template_def } },
        })
    end
end
--- Add a prompt to a server, creating the server if it doesn't exist
---@param server_name string Name of the server
---@param prompt_def MCPPrompt Prompt definition including name and handler
---@return NativeServer|nil server The server instance or nil on error
function Native.add_prompt(server_name, prompt_def)
    local result = validate.validate_prompt(prompt_def)
    if not result.ok then
        handle_error(result.error)
        return nil
    end

    local server = Native.is_native_server(server_name)
    if server then
        table.insert(server.capabilities.prompts, prompt_def)
        return server
    else
        return Native.add_server(server_name, {
            name = server_name,
            capabilities = { prompts = { prompt_def } },
        })
    end
end

Native.done_setup = false
function Native.setup(user_native_servers)
    if Native.done_setup then
        return
    end
    require("mcphub.native.neovim")
    require("mcphub.native.mcphub")
    -- Initialize native servers if any provided in setup config
    if user_native_servers and type(user_native_servers) == "table" then
        for name, def in pairs(user_native_servers) do
            local server = Native.register(def)
            if server then
                -- make sure the server name is set to key
                server.name = name
            end
        end
    end
    Native.done_setup = true
end

return Native

---@brief [[
--- Validation utilities for MCPHub
--- Handles configuration and input validation
---@brief ]]
local Error = require("mcphub.utils.errors")
local version = require("mcphub.utils.version")

local M = {}

---@class MCPServersConfigFile
---@field mcpServers table<string,MCPServerConfig>
---@field nativeMCPServers table<string,NativeMCPServerConfig>

---@class ValidationResult
---@field ok boolean
---@field error? MCPError

--- Validate setup options
---@param opts MCPHub.Config
---@return ValidationResult
function M.validate_setup_opts(opts)
    if not opts.port then
        return {
            ok = false,
            error = Error("SETUP", Error.Types.SETUP.INVALID_PORT, "Port is required for MCPHub setup"),
        }
    end

    if not opts.config or type(opts.config) ~= "string" then
        return {
            ok = false,
            error = Error("SETUP", Error.Types.SETUP.INVALID_CONFIG, "Config file path is required"),
        }
    end

    -- Validate cmd and cmdArgs if provided
    if opts.cmd and type(opts.cmd) ~= "string" then
        return {
            ok = false,
            error = Error("SETUP", Error.Types.SETUP.INVALID_CMD, "cmd must be a string"),
        }
    end

    if opts.cmdArgs and type(opts.cmdArgs) ~= "table" then
        return {
            ok = false,
            error = Error("SETUP", Error.Types.SETUP.INVALID_CMD_ARGS, "cmdArgs must be an array"),
        }
    end
    -- Validate native servers if present
    if opts.native_servers then
        if type(opts.native_servers) ~= "table" then
            return {
                ok = false,
                error = Error("SETUP", Error.Types.SETUP.INVALID_CONFIG, "native_servers must be a table"),
            }
        end
    end
    return { ok = true }
end

--- Validate server configuration
---@param name string Server name to validate
---@param config MCPServerConfig Server configuration
---@return ValidationResult
function M.validate_server_config(name, config)
    if type(config) ~= "table" then
        return {
            ok = false,
            error = Error(
                "VALIDATION",
                Error.Types.SETUP.INVALID_CONFIG,
                string.format("Server '%s' config must be a table", name)
            ),
        }
    end

    local has_stdio = config.command ~= nil
    local has_sse = config.url ~= nil

    -- Check for mixed fields
    if has_stdio and has_sse then
        return {
            ok = false,
            error = Error(
                "VALIDATION",
                Error.Types.SETUP.INVALID_CONFIG,
                string.format("Server '%s' cannot mix stdio and sse fields", name)
            ),
        }
    end

    -- Validate stdio config
    if has_stdio then
        if type(config.command) ~= "string" or config.command == "" then
            return {
                ok = false,
                error = Error(
                    "VALIDATION",
                    Error.Types.SETUP.INVALID_CONFIG,
                    string.format("Server '%s' has invalid or missing command", name)
                ),
            }
        end

        if config.args and not vim.islist(config.args) then
            return {
                ok = false,
                error = Error(
                    "VALIDATION",
                    Error.Types.SETUP.INVALID_CONFIG,
                    string.format("Server '%s' args must be an array", name)
                ),
            }
        end

        if config.env and type(config.env) ~= "table" then
            return {
                ok = false,
                error = Error(
                    "VALIDATION",
                    Error.Types.SETUP.INVALID_CONFIG,
                    string.format("Server '%s' has invalid environment config", name)
                ),
            }
        end

        if config.cwd and (type(config.cwd) ~= "string" or config.cwd == "") then
            return {
                ok = false,
                error = Error(
                    "VALIDATION",
                    Error.Types.SETUP.INVALID_CONFIG,
                    string.format("Server '%s' has invalid cwd: must be a non-empty string", name)
                ),
            }
        end
    elseif has_sse then
        -- Validate SSE config
        if type(config.url) ~= "string" or config.url == "" then
            return {
                ok = false,
                error = Error(
                    "VALIDATION",
                    Error.Types.SETUP.INVALID_CONFIG,
                    string.format("Server '%s' has invalid or missing url", name)
                ),
            }
        end

        -- Try parsing URL
        local ok, err = pcall(function()
            vim.uri_from_fname(config.url)
        end)
        if not ok then
            return {
                ok = false,
                error = Error(
                    "VALIDATION",
                    Error.Types.SETUP.INVALID_CONFIG,
                    string.format("Server '%s' has invalid url format: %s", name, err)
                ),
            }
        end

        if config.headers and type(config.headers) ~= "table" then
            return {
                ok = false,
                error = Error(
                    "VALIDATION",
                    Error.Types.SETUP.INVALID_CONFIG,
                    string.format("Server '%s' has invalid headers config", name)
                ),
            }
        end
    else
        return {
            ok = false,
            error = Error(
                "VALIDATION",
                Error.Types.SETUP.INVALID_CONFIG,
                string.format("Server '%s' must include either command (for stdio) or url (for sse)", name)
            ),
        }
    end

    return { ok = true }
end

--- Validate MCP Hub version
---@param ver_str string Version string to validate
---@return ValidationResult
function M.validate_version(ver_str)
    local major, minor, patch = ver_str:match("(%d+)%.(%d+)%.(%d+)")
    if not major then
        return {
            ok = false,
            error = Error("SETUP", Error.Types.SETUP.VERSION_MISMATCH, "Invalid version format", {
                version = ver_str,
            }),
        }
    end

    local current = {
        major = tonumber(major),
        minor = tonumber(minor),
        patch = tonumber(patch),
    }

    local required = version.REQUIRED_NODE_VERSION
    if current.major ~= required.major or current.minor < required.minor then
        return {
            ok = false,
            error = Error(
                "SETUP",
                Error.Types.SETUP.VERSION_MISMATCH,
                string.format("Incompatible mcp-hub version. Found %s, required %s", ver_str, required.string),
                {
                    found = ver_str,
                    required = required.string,
                    install_cmd = string.format("npm install -g mcp-hub@%s", required.string),
                }
            ),
        }
    end

    return {
        ok = true,
    }
end

--- Validate a property according to requirements
---@param value any The value to validate
---@param prop_type string Type of property (string|function|table)
---@param name string Name of property for error messages
---@param error_type string Error type from Error.Types.NATIVE
---@param object_id? string Optional identifier for error messages
---@param extra_check? function Optional additional validation function
---@return ValidationResult
local function validate_property(value, prop_type, name, error_type, object_id, extra_check)
    if not value or type(value) ~= prop_type then
        return {
            ok = false,
            error = Error("VALIDATION", error_type, string.format("%s must be a %s", name, prop_type)),
        }
    end

    if prop_type == "string" and value == "" then
        return {
            ok = false,
            error = Error("VALIDATION", error_type, string.format("%s cannot be empty", name)),
        }
    end

    if extra_check then
        local ok, err = extra_check(value)
        if not ok then
            return {
                ok = false,
                error = Error(
                    "VALIDATION",
                    error_type,
                    string.format("%s: %s", object_id and string.format("%s in %s", err, object_id) or err, name)
                ),
            }
        end
    end

    return { ok = true }
end

---inputSchema will be evaluated if it is a function and is obj is validated
---@param inputSchema table
---@param tool_name string
function M.validate_inputSchema(inputSchema, tool_name)
    if not inputSchema then
        return { ok = true }
    end
    -- Check inputSchema structure
    local function validate_schema(schema)
        if schema.type ~= "object" then
            return false, "type must be 'object'"
        end
        if schema.properties and type(schema.properties) ~= "table" then
            return false, "must have a properties table"
        end
        return true
    end

    return validate_property(
        inputSchema,
        "table",
        "Input schema",
        Error.Types.NATIVE.INVALID_SCHEMA,
        tool_name or "",
        validate_schema
    )
end

--- Validate a tool definition
---@param tool MCPTool Tool definition to validate
---@return ValidationResult
function M.validate_tool(tool)
    -- Validate name
    local name_result = validate_property(tool.name, "string", "Tool name", Error.Types.NATIVE.INVALID_NAME)
    if not name_result.ok then
        return name_result
    end

    -- Validate handler
    local handler_result =
        validate_property(tool.handler, "function", "Handler", Error.Types.NATIVE.INVALID_HANDLER, tool.name)
    if not handler_result.ok then
        return handler_result
    end
    return { ok = true }
end

--- Validate a resource definition
---@param resource MCPResource Resource definition to validate
---@return ValidationResult
function M.validate_resource(resource)
    local name_result = validate_property(resource.name, "string", "Resource Name", Error.Types.NATIVE.INVALID_NAME)
    if not name_result.ok then
        return name_result
    end
    -- Validate URI
    local uri_result = validate_property(resource.uri, "string", "Resource URI", Error.Types.NATIVE.INVALID_URI)
    if not uri_result.ok then
        return uri_result
    end

    -- Validate handler
    local handler_result =
        validate_property(resource.handler, "function", "Handler", Error.Types.NATIVE.INVALID_HANDLER, resource.uri)
    if not handler_result.ok then
        return handler_result
    end

    return { ok = true }
end

--- Validate a resource template definition
---@param template MCPResourceTemplate Resource template definition to validate
---@return ValidationResult
function M.validate_resource_template(template)
    -- Validate URI template
    local function validate_uri_template(uri)
        if not uri:match("{[^}]+}") then
            return false, "must contain at least one parameter in {param} format"
        end
        return true
    end

    local name_result = validate_property(template.name, "string", "Resource Name", Error.Types.NATIVE.INVALID_NAME)
    if not name_result.ok then
        return name_result
    end
    local uri_result = validate_property(
        template.uriTemplate,
        "string",
        "URI template",
        Error.Types.NATIVE.INVALID_URI,
        nil,
        validate_uri_template
    )
    if not uri_result.ok then
        return uri_result
    end

    -- Validate handler
    local handler_result = validate_property(
        template.handler,
        "function",
        "Handler",
        Error.Types.NATIVE.INVALID_HANDLER,
        template.uriTemplate
    )
    if not handler_result.ok then
        return handler_result
    end

    return { ok = true }
end

-- Add prompts validation in native server validation
---@param def NativeServerDef Native server definition to validate
---@return {ok: boolean, error: MCPError}
function M.validate_native_server(def)
    local server_name = def.name
    if not def.name then
        return {
            ok = false,
            error = Error("SETUP", Error.Types.SETUP.INVALID_CONFIG, "Native server must contain a name"),
        }
    end
    if not def.capabilities then
        return {
            ok = false,
            error = Error(
                "SETUP",
                Error.Types.SETUP.INVALID_CONFIG,
                string.format("Native server '%s' must contain capabilities", server_name)
            ),
        }
    end
    -- Validate tools if present
    if def.capabilities.tools then
        for _, tool in ipairs(def.capabilities.tools) do
            local ok, err = M.validate_tool(tool)
            if not ok then
                return {
                    ok = false,
                    error = err,
                }
            end
        end
    end

    -- Validate resources if present
    if def.capabilities.resources then
        for _, resource in ipairs(def.capabilities.resources) do
            local ok, err = M.validate_resource(resource)
            if not ok then
                return {
                    ok = false,
                    error = err,
                }
            end
        end
    end

    -- Validate resource templates if present
    if def.capabilities.resourceTemplates then
        for _, template in ipairs(def.capabilities.resourceTemplates) do
            local ok, err = M.validate_resource_template(template)
            if not ok then
                return {
                    ok = false,
                    error = err,
                }
            end
        end
    end

    -- Validate prompts if present
    if def.capabilities.prompts then
        for _, prompt in ipairs(def.capabilities.prompts) do
            local ok, err = M.validate_prompt(prompt)
            if not ok then
                return {
                    ok = false,
                    error = err,
                }
            end
        end
    end
    return { ok = true }
end

--- Validate a prompt definition
---@param prompt MCPPrompt Prompt definition to validate
---@return ValidationResult
function M.validate_prompt(prompt)
    -- Validate name
    local name_result = validate_property(prompt.name, "string", "Prompt name", Error.Types.NATIVE.INVALID_NAME)
    if not name_result.ok then
        return name_result
    end

    -- Validate handler
    local handler_result =
        validate_property(prompt.handler, "function", "Handler", Error.Types.NATIVE.INVALID_HANDLER, prompt.name)
    if not handler_result.ok then
        return handler_result
    end

    return { ok = true }
end

return M

---@class BaseResponse
---@field output_handler function|nil Async callback handler
---@field error fun(self: BaseResponse, message: string, details?: table): table Send error response
---@field result table Response data
local BaseResponse = {}
BaseResponse.__index = BaseResponse

function BaseResponse:new(output_handler)
    local instance = {
        output_handler = output_handler,
        result = {},
    }
    return setmetatable(instance, self)
end

function BaseResponse:send(result)
    local final_result = result or self.result
    if self.output_handler then
        -- Async with callback
        self.output_handler({
            result = final_result,
        })
    else
        -- Sync return
        return {
            result = final_result,
        }
    end
end

---@alias MCPResourceContent { uri: string, text?: string, blob?: string, mimeType: string }
---@alias MCPContent { type: "text"|"image"|"audio"|"resource", text?: string, data?: string, resource?: MCPResourceContent, mimeType?: string }

---@class ToolResponse : BaseResponse
---@field result { content: MCPContent[] }
---@field text fun(self: ToolResponse, text: string): ToolResponse Add text content
---@field image fun(self: ToolResponse, data: string, mime: string): ToolResponse Add image content
---@field audio fun(self: ToolResponse, data: string, mime: string): ToolResponse Add audio content
---@field resource fun(self: ToolResponse, resource: MCPResourceContent): ToolResponse Add resource content
---@field error fun(self: ToolResponse|BaseResponse, message: string, details?: table): table Send error response
---@field send fun(self: ToolResponse, result?: table): table Send response
local ToolResponse = setmetatable({}, { __index = BaseResponse })
ToolResponse.__index = ToolResponse

function ToolResponse:new(output_handler)
    local instance = BaseResponse:new(output_handler)
    instance.result = { content = {} }
    setmetatable(instance, self)
    return instance
end

function ToolResponse:text(text)
    if type(text) ~= "string" then
        text = vim.inspect(text)
    end
    table.insert(self.result.content, {
        type = "text",
        text = text,
    })
    return self
end

function ToolResponse:image(data, mime)
    table.insert(self.result.content, {
        type = "image",
        data = data,
        mimeType = mime,
    })
    return self
end

function ToolResponse:audio(data, mime)
    table.insert(self.result.content, {
        type = "audio",
        data = data,
        mimeType = mime or "audio/mp3",
    })
    return self
end

function ToolResponse:resource(resource)
    table.insert(self.result.content, {
        type = "resource",
        resource = resource,
    })
    return self
end

function ToolResponse:error(message, details)
    if type(message) ~= "string" then
        message = vim.inspect(message)
    end
    local result = {
        isError = true,
        content = {
            {
                type = "text",
                text = message,
            },
        },
    }

    -- Add details if provided
    if details then
        table.insert(result.content, {
            type = "text",
            text = "Details: " .. vim.inspect(details),
        })
    end

    -- Auto-send error response
    return self:send(result)
end

---@class ResourceResponse : BaseResponse
local ResourceResponse = setmetatable({}, { __index = BaseResponse })
ResourceResponse.__index = ResourceResponse

---@return ResourceResponse
function ResourceResponse:new(output_handler, uri, template)
    ---@class BaseResponse
    local instance = BaseResponse:new(output_handler)
    instance.uri = uri
    instance.template = template
    instance.result = { contents = {} }
    setmetatable(instance, self)
    return instance --[[@as ResourceResponse]]
end

function ResourceResponse:text(text, mime)
    if type(text) ~= "string" then
        text = vim.inspect(text)
    end
    table.insert(self.result.contents, {
        uri = self.uri,
        text = text,
        mimeType = mime or "text/plain",
    })
    return self
end

function ResourceResponse:blob(data, mime)
    table.insert(self.result.contents, {
        uri = self.uri,
        blob = data,
        mimeType = mime or "application/octet-stream",
    })
    return self
end

function ResourceResponse:image(data, mime)
    table.insert(self.result.contents, {
        uri = self.uri,
        blob = data,
        mimeType = mime or "image/png",
    })
    return self
end

function ResourceResponse:audio(data, mime)
    table.insert(self.result.contents, {
        uri = self.uri,
        blob = data,
        mimeType = mime or "audio/mp3",
    })
    return self
end

function ResourceResponse:error(message, details)
    if type(message) ~= "string" then
        message = vim.inspect(message)
    end
    -- For resources, we return error as a text resource
    self.result = {
        contents = {
            {
                uri = self.uri,
                text = message .. (details and ("\nDetails: " .. vim.inspect(details)) or ""),
                mimeType = "text/plain",
            },
        },
    }
    return self:send(self.result)
end

---@class PromptResponse : BaseResponse
local PromptResponse = setmetatable({}, { __index = BaseResponse })
PromptResponse.__index = PromptResponse

function PromptResponse:new(output_handler, name, description)
    ---@class BaseResponse
    local instance = BaseResponse:new(output_handler)
    instance.name = name
    instance.description = description
    instance.result = { messages = {} }
    instance.current_role = "user" -- Default role is user
    setmetatable(instance, self)
    return instance
end

-- Role state setters
function PromptResponse:user()
    self.current_role = "user"
    return self
end

function PromptResponse:llm()
    self.current_role = "assistant"
    return self
end

function PromptResponse:system()
    self.current_role = "system"
    return self
end

function PromptResponse:text(text)
    if type(text) ~= "string" then
        text = vim.inspect(text)
    end
    table.insert(self.result.messages, {
        role = self.current_role,
        content = {
            type = "text",
            text = text,
        },
    })
    return self
end

function PromptResponse:image(data, mime)
    table.insert(self.result.messages, {
        role = self.current_role,
        content = {
            type = "image",
            data = data,
            mimeType = mime or "image/png",
        },
    })
    return self
end

function PromptResponse:blob(data, mime)
    table.insert(self.result.messages, {
        role = self.current_role,
        content = {
            type = "blob",
            data = data,
            mimeType = mime or "application/octet-stream",
        },
    })
    return self
end

function PromptResponse:audio(data, mime)
    table.insert(self.result.messages, {
        role = self.current_role,
        content = {
            type = "audio",
            data = data,
            mimeType = mime or "audio/mp3",
        },
    })
    return self
end

function PromptResponse:resource(resource)
    table.insert(self.result.messages, {
        role = self.current_role,
        content = {
            type = "resource",
            resource = resource,
        },
    })
    return self
end

function PromptResponse:error(message, details)
    if type(message) ~= "string" then
        message = vim.inspect(message)
    end
    -- Switch to system role for error, then send
    self:user():text(message .. (details and ("\nDetails: " .. vim.inspect(details)) or ""))
    return self:send(self.result)
end

return {
    ToolResponse = ToolResponse,
    ResourceResponse = ResourceResponse,
    PromptResponse = PromptResponse,
}

local M = {}

local SHUTDOWN_DELAY = 5 * 60 * 1000 -- 5 minutes

---@class MCPHub.WorkspaceConfig
---@field enabled boolean Master switch for workspace-specific hubs
---@field look_for string[] Files to search for (in order)
---@field port_range { min: number, max: number } Port range for workspace hubs
---@field reload_on_dir_changed boolean Whether to listen to DirChanged events to reload workspace config
---@field get_port fun(): number | nil Function that determines that returns the port

---@class MCPHub.Config
local defaults = {
    port = 37373, -- Default port for MCP Hub
    server_url = nil, -- In cases where mcp-hub is hosted somewhere, set this to the server URL e.g `http://mydomain.com:customport` or `https://url_without_need_for_port.com`
    config = vim.fn.expand("~/.config/mcphub/servers.json"), -- Default config location
    shutdown_delay = SHUTDOWN_DELAY, -- Delay before shutting down the mcp-hub
    mcp_request_timeout = 60000, --Timeout for MCP requests in milliseconds, useful for long running tasks
    ---@type table<string, NativeServerDef>
    native_servers = {},
    builtin_tools = {
        ---@type EditSessionConfig
        edit_file = {
            parser = {
                track_issues = true,
                extract_inline_content = true,
            },
            locator = {
                fuzzy_threshold = 0.8,
                enable_fuzzy_matching = true,
            },
            ui = {
                go_to_origin_on_complete = true,
                keybindings = {
                    accept = ".", -- Accept current change
                    reject = ",", -- Reject current change
                    next = "n", -- Next diff
                    prev = "p", -- Previous diff
                    accept_all = "ga", -- Accept all remaining changes
                    reject_all = "gr", -- Reject all remaining changes
                },
            },
            feedback = {
                include_parser_feedback = true,
                include_locator_feedback = true,
                include_ui_summary = true,
                ui = {
                    include_session_summary = true,
                    include_final_diff = true,
                    send_diagnostics = true,
                    wait_for_diagnostics = 500,
                    diagnostic_severity = vim.diagnostic.severity.WARN, -- Only show warnings and above by default
                },
            },
        },
    },
    --- Custom function to parse json file (e.g `require'json5'.parse` from `https://github.com/Joakker/lua-json5 to parse json5 syntax for .vscode/mcp.json like files)
    ---@type function | nil
    json_decode = nil,
    ---@type boolean | fun(parsed_params: MCPHub.ParsedParams): boolean | nil | string  Function to determine if a call should be auto-approved
    auto_approve = false,
    auto_toggle_mcp_servers = true, -- Let LLMs start and stop MCP servers automatically
    use_bundled_binary = false, -- Whether to use bundled mcp-hub binary
    ---@type table | fun(context: MCPHub.JobContext): table Global environment variables available to all MCP servers
    global_env = {}, -- Environment variables that will be available to all MCP servers
    ---@type string?
    cmd = nil, -- will be set based on system if not provided
    ---@type table?
    cmdArgs = nil, -- will be set based on system if not provided
    ---@type LogConfig
    log = {
        level = vim.log.levels.ERROR,
        to_file = false,
        file_path = nil,
        prefix = "MCPHub",
    },
    ---@type MCPHub.UIConfig
    ui = {
        window = {},
        wo = {},
    },
    ---@type MCPHub.Extensions.Config
    extensions = {
        avante = {
            enabled = true,
            make_slash_commands = true,
        },
    },
    ---@type MCPHub.WorkspaceConfig
    workspace = {
        enabled = true, -- Enables workspace-specific hubs
        look_for = { ".mcphub/servers.json", ".vscode/mcp.json", ".cursor/mcp.json" }, -- Files to search for (in order)
        reload_on_dir_changed = true, -- Whether to listen to DirChanged events to reload workspace config
        port_range = { min = 40000, max = 41000 }, -- Port range for workspace hubs
        -- function that determines that returns the port
        --- @type fun(): number | nil
        get_port = nil,
    },
    on_ready = function() end,
    ---@param msg string
    on_error = function(msg) end,
}

---@param opts MCPHub.Config?
---@return MCPHub.Config
function M.setup(opts)
    M.config = vim.tbl_deep_extend("force", defaults, opts or {})
    return M.config
end

return setmetatable(M, {
    __index = function(_, key)
        if key == "setup" then
            return M.setup
        end
        return rawget(M.config, key)
    end,
})

-- Type definitions for the Editor system
-- All classes reference these unified type structures

local M = {}

-- Enums for type safety and better IntelliSense
---@enum OverallMatchType
M.OVERALL_MATCH_TYPE = {
    exact = "exact",
    exact_whitespace = "exact_whitespace",
    fuzzy_high = "fuzzy_high",
    fuzzy_medium = "fuzzy_medium",
    fuzzy_low = "fuzzy_low",
}

---@enum LineMatchType
M.LINE_MATCH_TYPE = {
    exact = "exact",
    exact_whitespace = "exact_whitespace",
    normalized = "normalized",
    punctuation = "punctuation",
    case_insensitive = "case_insensitive",
    fuzzy_high = "fuzzy_high",
    fuzzy_medium = "fuzzy_medium",
    fuzzy_low = "fuzzy_low",
    no_match = "no_match",
}

---@enum SearchMethod
M.SEARCH_METHOD = {
    exact_scan = "exact_scan",
    fuzzy_search = "fuzzy_search",
    middle_out = "middle_out",
    linear = "linear",
}

---@enum SearchStrategy
M.SEARCH_STRATEGY = {
    auto = "auto",
    linear = "linear",
    middle_out = "middle_out",
}

---@enum DiffHunkType
M.DIFF_HUNK_TYPE = {
    addition = "addition",
    deletion = "deletion",
    change = "change",
}

---@enum BlockStatus
M.BLOCK_STATUS = {
    ready = "ready",
    failed = "failed",
    skipped = "skipped",
}

---@enum DifferenceType
M.DIFFERENCE_TYPE = {
    quote_style = "quote_style",
    whitespace = "whitespace",
    case = "case",
    punctuation = "punctuation",
    html_entities = "html_entities",
}

---@class BlockLocationResult
---@field found boolean Whether block was located in file
---@field start_line integer? 1-based start line (nil if not found)
---@field end_line integer? 1-based end line (nil if not found)
---@field overall_score number Overall match score (0.0-1.0)
---@field overall_match_type OverallMatchType Match type classification
---@field confidence integer Overall confidence percentage (0-100)
---@field found_content string? Actual content found in file
---@field found_lines string[]? Actual lines found in file
---@field line_details LineMatchDetail[] Per-line match information
---@field search_metadata SearchMetadata Performance and method info
---@field error string? Error message if not found

---@class LineMatchDetail
---@field line_number integer Absolute line number in file
---@field expected_line string What we were searching for
---@field found_line string What was actually in the file
---@field line_score number Match score for this line (0.0-1.0)
---@field line_match_type LineMatchType Match type for this specific line
---@field differences DifferenceType[]? List of difference types found

---@class SearchMetadata
---@field method SearchMethod Search method used to find the block
---@field iterations integer Number of search iterations performed
---@field search_strategy SearchStrategy Search strategy that was employed
---@field search_bounds integer[] Start and end bounds used for search
---@field early_termination boolean Whether search stopped early due to good match

---@class ParsedBlock
---@field search_content string Exact content to find
---@field replace_content string Replacement content
---@field block_id string Unique identifier for this block
---@field search_lines string[] Split search content into lines
---@field replace_lines string[] Split replace content into lines

---@class LocatedBlock
---@field search_content string Exact content to find
---@field replace_content string Replacement content
---@field block_id string Unique identifier for this block
---@field search_lines string[] Split search content into lines
---@field replace_lines string[] Split replace content into lines
---@field location_result BlockLocationResult Complete location information
---@field applied_start_line integer? 1-based start line in file (nil if not found)
---@field applied_end_line integer? 1-based end line in file (nil if not found)
---@field applied_hunk_ranges table<integer, integer>[]? List of {start_line, end_line} pairs for applied hunks
---@field old_ext_id integer? External mark ID for old content (if applicable) -- DEPRECATED (replaced by hunk_extmarks)
---@field new_ext_id integer? External mark ID for new content (if applicable) -- DEPRECATED (replaced by hunk_extmarks)

---@class DiffHunk
---@field hunk_id string Unique identifier for this hunk
---@field parent_id string Parent block ID if this hunk is part of a larger block
---@field old_lines string[] Lines being removed/changed
---@field new_lines string[] Lines being added/changed
---@field extmark_id integer? External mark ID for this hunk (nil if not set)
---@field del_extmark_id integer? External mark ID for virtual lines added to this hunk
---@field deletion_position integer? If this hunk is a deletion, the line number where it was removed (nil if not a deletion)
---@field type DiffHunkType Type of change in this hunk
---@field applied_start_line integer 1-based start line in file (nil if not applied)
---@field applied_end_line integer 1-based end line in file (nil if not applied)
---@field confidence integer Confidence percentage for this hunk (0-100)

---@class FuzzyHighlight
---@field line_num integer Absolute line number in file
---@field highlight_type string Highlight group name
---@field expected string What was expected
---@field actual string What was actually found
---@field reason string Reason for mismatch
---@field char_positions integer[]? Character positions that differ

---@class ContextInfo
---@field start_line integer Context start line for UI display
---@field end_line integer Context end line for UI display
---@field surrounding_lines string[]? Additional context lines if needed

---@class ParsingIssue
---@field type string Issue type identifier
---@field details table Issue-specific details
---@field timestamp integer When the issue was tracked
---@field severity string "warning" | "error" | "info"
---@field description string Human-readable description of the issue
---@field fix string Suggested fix or action to take
---@field llm_guidance string Guidance for LLM feedback on this issue

---@class EditSessionConfig
---@field parser ParserConfig? Parser-specific configuration
---@field locator LocatorConfig? Block locator configuration
---@field analyzer AnalyzerConfig? Diff analyzer configuration
---@field ui UIConfig? UI-specific configuration
---@field feedback table Feedback configuration for LLM

---@class ParserConfig
---@field track_issues boolean Track parsing issues for LLM feedback
---@field extract_inline_content boolean Handle content on marker lines

---@class LocatorConfig
---@field fuzzy_threshold number Minimum similarity score for fuzzy matches (0.0-1.0)
---@field enable_fuzzy_matching boolean Allow fuzzy matching when exact fails

---@class AnalyzerConfig
---@field diff_algorithm string "patience" | "minimal" | "histogram"
---@field context_lines integer Number of context lines to include
---@field show_char_diff boolean Show character-level differences

---@class UIKeybindings
---@field accept string Key to accept changes
---@field reject string Key to reject changes
---@field next string Key to navigate to next block
---@field prev string Key to navigate to previous block
---@field accept_all string Key to accept all changes
---@field reject_all string Key to reject all changes

---@class UIConfig
---@field go_to_origin_on_complete boolean Jump back to original file on completion
---@field keybindings UIKeybindings Custom keybindings for UI actions
return M

---@diagnostic disable: deprecated
local start = vim.health.start or vim.health.report_start --[[@as function]]
local ok = vim.health.ok or vim.health.report_ok --[[@as function]]
local info = vim.health.info or vim.health.report_info --[[@as function]]
local warn = vim.health.warn or vim.health.report_warn --[[@as function]]
local error = vim.health.error or vim.health.report_error --[[@as function]]

local fmt = string.format

local M = {}

M.deps = {
    {
        name = "plenary.nvim",
        plugin_name = "plenary",
    },
}

M.libraries = {
    {
        name = "curl",
    },
    {
        name = "node",
    },
    {
        name = "uv",
        optional = true,
        failed_msg = fmt("uv not found: Install with `%s`", "curl -LsSf https://astral.sh/uv/install.sh | sh"),
    },
}

M.chat_plugins = {
    {
        name = "codecompanion.nvim",
        plugin_name = "codecompanion",
        optional = true,
    },
    {
        name = "avante.nvim",
        plugin_name = "avante",
        optional = true,
    },
    {
        name = "CopilotChat.nvim",
        plugin_name = "CopilotChat",
        optional = true,
    },
}

local function plugin_available(name)
    local check, _ = pcall(require, name)
    return check
end

local function lib_available(lib)
    if vim.fn.executable(lib) == 1 then
        return true
    end
    return false
end

function M.check()
    start("mcphub.nvim")

    local version = require("mcphub.utils.version")
    info(fmt("mcphub.nvim version: %s", version.PLUGIN_VERSION))
    info("mcp-hub binary:")

    --find mcp-hub
    local State = require("mcphub.state")
    local validation = require("mcphub.utils.validation")
    local merged_config = State.config
    local cmd = merged_config.cmd
    local cmdArgs = merged_config.cmdArgs
    local mcp_hub_path = cmd .. " " .. table.concat(cmdArgs or {}, " ")
    local required_version = version.REQUIRED_NODE_VERSION.string
    info("  mcp-hub required version: " .. required_version)
    local installed_version = vim.fn.system(mcp_hub_path .. " --version")
    installed_version = vim.trim(installed_version)
    if vim.v.shell_error ~= 0 then
        error(fmt("mcp-hub not found: %s", mcp_hub_path))
    else
        if installed_version == required_version then
            info("  mcp-hub installed version: " .. installed_version)
        else
            warn(fmt("  mcp-hub installed version: %s", installed_version))
        end
    end
    local validation_result = validation.validate_version(installed_version)
    if not validation_result.ok then
        error(fmt("mcp-hub version not compatible: %s", validation_result.error.message))
    else
        ok(fmt("mcp-hub version %s is compatible", installed_version))
    end

    start("Plugin Dependencies:")
    for _, plugin in ipairs(M.deps) do
        if plugin_available(plugin.plugin_name) then
            ok(fmt("%s installed", plugin.name))
        else
            if plugin.optional then
                warn(plugin.failed_msg or fmt("%s not found", plugin.name))
            else
                error(plugin.failed_msg or fmt("%s not found", plugin.name))
            end
        end
    end

    start("Libraries:")

    for _, library in ipairs(M.libraries) do
        if lib_available(library.name) then
            ok(fmt("%s installed", library.name))
        else
            if library.optional then
                warn(library.failed_msg or fmt("%s not found", library.name))
            else
                error(library.failed_msg or fmt("%s not found", library.name))
            end
        end
    end

    start("Chat plugins")
    for _, plugin in ipairs(M.chat_plugins) do
        if plugin_available(plugin.plugin_name) then
            ok(fmt("%s installed", plugin.name))
        else
            if plugin.optional then
                warn(plugin.failed_msg or fmt("%s not found", plugin.name))
            else
                error(plugin.failed_msg or fmt("%s not found", plugin.name))
            end
        end
    end
end

return M

---@meta
---@class MarketplaceItem
---@field id string
---@field name string
---@field author string
---@field description string
---@field url string
---@field license? string
---@field category string
---@field tags string[]
---@field installations MarketplaceInstallation[]
---@field featured? boolean
---@field verified? boolean
---@field stars? integer
---@field lastCommit? integer
---@field updatedAt? integer

---@class MarketplaceInstallation
---@field name string
---@field description? string
---@field config string
---@field prerequisites? string[]
---@field parameters? MarketplaceParameter[]
---@field transports? string[]

---@class MarketplaceParameter
---@field name string
---@field key string
---@field description? string
---@field placeholder? string
---@field required? boolean

---@class CustomMCPServerConfig.CustomInstructions
---@field text string
---@field disabled? string

---@class CustomMCPServerConfig
---@field disabled? boolean
---@field disabled_tools? string[]
---@field disabled_prompts? string[]
---@field disabled_resources? string[]
---@field disabled_resourceTemplates? string[]
---@field custom_instructions? CustomMCPServerConfig.CustomInstructions
---@field autoApprove? boolean|string[] -- true for all tools, array of tool names for specific tools

---@class MCPServerConfig: CustomMCPServerConfig
---@field command? string
---@field args? table
---@field env? table<string,string>
---@field cwd? string
---@field headers? table<string,string>
---@field url? string

---@class NativeMCPServerConfig : CustomMCPServerConfig

---@class MCPServer
---@field name string
---@field displayName string
---@field description string
---@field transportType string
---@field status MCPHub.Constants.ConnectionStatus
---@field error string
---@field capabilities MCPCapabilities
---@field uptime number
---@field lastStarted string
---@field authorizationUrl string
---@field config_source string Include which config file this server came from

---@class LogEntry
---@field type string
---@field message string
---@field timestamp number
---@field data table<string,any>
---@field code number

---@class MCPResponseOutput
---@field text string
---@field images table[]
---@field blobs table[]
---@field audios table[]
---@field error? string

---@class EnhancedMCPPrompt : MCPPrompt
---@field server_name string

---@class EnhancedMCPResourceTemplate : MCPResourceTemplate
---@field server_name string

---@class EnhancedMCPResource : MCPResource
---@field server_name string

---@class EnhancedMCPTool : MCPTool
---@field server_name string

---@class MCPRequestOptions
---@field timeout? number
---@field resetTimeoutOnProgress? boolean
---@field maxTotalTimeout? number

---@class MCPHub.JobContext
---@field cwd string -- Current working directory for the job
---@field port number -- Port to connect to the MCP server
---@field config_files string[] -- List of configuration files used to start the Hub including the project config and global
---@field is_workspace_mode boolean -- Whether the job is running in workspace mode
---@field workspace_root string? -- Root directory of the workspace if in workspace mode
---@field existing_hub MCPHub.WorkspaceDetails? -- Details of an existing hub if applicable

---@class MCPHub.Workspaces
---@field current string? -- Name of the current workspace
---@field allActive table<string, MCPHub.WorkspaceDetails>? -- Map of workspace names to their details

---@class MCPHub.WorkspaceDetails
---@field pid number -- Process ID of the mcp-hub server
---@field port number -- Port the mcp-hub server is running on
---@field config_files string[] -- List of configuration files used to start the Hub including the project config and global
---@field startTime string -- ISO 8601 formatted start time of the workspace
---@field cwd string -- Current working directory of the mcp-hub
---@field shutdownDelay number? -- Optional delay before the workspace is shut down after inactivity
---@field state "active" | "shutting_down" -- Current state of the workspace
---@field activeConnections number -- Number of active connections to the workspace

local M = {}

--- Safely get keymap info for a specific key in a buffer
---@param mode string The mode (e.g., 'n', 'i', 'v')
---@param lhs string The left-hand side of the mapping
---@param buffer integer Buffer number
---@return table|nil Original keymap info or nil if not found
function M.get_keymap_info(mode, lhs, buffer)
    local maps = vim.api.nvim_buf_get_keymap(buffer, mode)
    for _, map in ipairs(maps) do
        if map.lhs == lhs then
            vim.notify("Found keymap: " .. lhs .. " in buffer " .. buffer, vim.log.levels.INFO)
            return map
        end
    end
    return nil
end

--- Restore a previously stored keymap
---@param mode string The mode (e.g., 'n', 'i', 'v')
---@param lhs string The left-hand side of the mapping
---@param buffer integer Buffer number
---@param original_map table|nil Original keymap info from get_keymap_info
function M.restore_keymap(mode, lhs, buffer, original_map)
    pcall(vim.keymap.del, mode, lhs, { buffer = buffer })
    if original_map then
        -- If there was an original mapping, restore it
        local opts = {
            buffer = buffer,
            desc = original_map.desc,
            nowait = original_map.nowait == 1,
            silent = original_map.silent == 1,
            expr = original_map.expr == 1,
        }

        if original_map.callback then
            vim.keymap.set(mode, lhs, original_map.callback, opts)
        elseif original_map.rhs then
            vim.keymap.set(mode, lhs, original_map.rhs, opts)
        end
    end
end

--- Store original keymaps for multiple keys
---@param mode string The mode (e.g., 'n', 'i', 'v')
---@param keys table<string, any> Table of key mappings where keys are lhs
---@param buffer integer Buffer number
---@return table<string, table|nil> Table of original keymap info
function M.store_original_keymaps(mode, keys, buffer)
    local original_maps = {}
    for lhs, _ in pairs(keys) do
        original_maps[lhs] = M.get_keymap_info(mode, lhs, buffer)
    end
    return original_maps
end

--- Restore multiple keymaps and clean up temporary ones
---@param mode string The mode (e.g., 'n', 'i', 'v')
---@param keys table<string, any> Table of key mappings where keys are lhs
---@param buffer integer Buffer number
---@param original_maps table<string, table|nil> Original keymap info from store_original_keymaps
function M.restore_keymaps(mode, keys, buffer, original_maps)
    for _, lhs in pairs(keys) do
        -- Restore original keymaps if they existed
        M.restore_keymap(mode, lhs, buffer, original_maps[lhs])
    end
end

return M

--[[ MCPHub image cache utilities ]]
local M = {}

-- Cache directory
M.cache_dir = vim.fn.stdpath("cache") .. "/mcphub/images"

local counter = 0
--- Get unique filename based on content hash
---@param data string Base64 encoded image data
---@param mime_type string MIME type of the image
---@return string filename
local function get_unique_filename(data, mime_type)
    -- local hash = vim.fn.sha256(data)
    local time = os.time() + counter
    counter = counter + 1
    local ext = mime_type:match("image/(%w+)") or "bin"
    return string.format("%s.%s", time, ext)
end
--- Save image to temp file and return file path
---@param data string Base64 encoded image data
---@param mime_type string MIME type of the image
---@return string|nil filepath Path to saved file
function M.save_image(data, mime_type)
    local filename = get_unique_filename(data, mime_type)
    local filepath = M.cache_dir .. "/" .. filename

    -- Open file with proper error handling
    local file, err = io.open(filepath, "wb")
    if not file then
        error(string.format("Failed to open file for writing: %s", err))
        return nil
    end

    local success, result = pcall(function()
        -- Try base64 decode if it looks like base64
        if type(data) == "string" and data:match("^[A-Za-z0-9+/]+=*$") then
            local ok, decoded = pcall(vim.base64.decode, data)
            if ok then
                file:write(decoded)
                return
            end
        end

        -- Handle binary/blob data
        if type(data) == "string" then
            file:write(data)
        else
            error("Unsupported data type: " .. type(data))
        end
    end)

    file:close()

    if not success then
        vim.fn.delete(filepath)
        error(string.format("Failed to write image data: %s", result))
        return nil
    end

    return filepath
end
--- Clean all cached images
function M.cleanup()
    -- Get all files in the cache directory
    local files = vim.fn.glob(M.cache_dir .. "/*", true, true)
    for _, file in ipairs(files) do
        vim.fn.delete(file)
    end
end

--- Initialize image cache
function M.setup()
    -- Create cache directory if it doesn't exist
    vim.fn.mkdir(M.cache_dir, "p")

    -- Setup cleanup on exit
    vim.api.nvim_create_autocmd("VimLeavePre", {
        group = vim.api.nvim_create_augroup("mcphub_image_cache", { clear = true }),
        callback = function()
            M.cleanup()
        end,
    })
end

return M

local buf_utils = require("mcphub.native.neovim.utils.buffer")
local os_utils = require("mcphub.native.neovim.utils.os")

---@type MCPResource
return {
    name = "Workspace",
    description = "This resource gives comprehensive information about the workspace, editor and OS. Includes directory structure, visible and loaded buffers along with the OS information.",
    uri = "neovim://workspace",
    mimeType = "text/plain",
    handler = function(req, res)
        local editor_info = req.editor_info
        local os_info = os_utils.get_os_info()
        local dir_info = buf_utils.get_directory_info(vim.fn.getcwd())

        -- Format visible and loaded buffers
        local visible = vim.tbl_map(
            function(buf)
                return string.format("%s%s", buf.name, buf.bufnr == editor_info.last_active.bufnr and " (active)" or "")
            end,
            vim.tbl_filter(function(buf)
                return buf.is_visible
            end, editor_info.buffers)
        )

        local loaded = vim.tbl_map(
            function(buf)
                return string.format("%s%s", buf.name, buf.bufnr == editor_info.last_active.bufnr and " (active)" or "")
            end,
            vim.tbl_filter(function(buf)
                return (not buf.is_visible) and buf.is_loaded
            end, editor_info.buffers)
        )

        -- Format workspace files
        local workspace_files = vim.tbl_map(function(file)
            return string.format("%s (%s, %.2fKB)", file.name, file.type, file.size / 1024)
        end, dir_info.files)

        local text = string.format(
            [[
<workspace_details>
## System Information
OS: %s (%s)
Hostname: %s
User: %s
Shell: %s
Memory: %.2f GB total, %.2f GB free

## Workspace
Current Directory: %s
Git Repository: %s
Files: %d

## Workspace Files
%s

## Neovim Visible Files
%s

## Neovim Loaded Files
%s

## Current Time
%s
</workspace_details>
            ]],
            os_info.os_name,
            os_info.arch,
            os_info.hostname,
            os_info.env.user,
            os_info.env.shell,
            os_info.memory.total / (1024 * 1024 * 1024),
            os_info.memory.free / (1024 * 1024 * 1024),
            os_info.cwd,
            dir_info.is_git and "Yes" or "No",
            #dir_info.files,
            table.concat(workspace_files, "\n"),
            table.concat(visible, "\n"),
            table.concat(loaded, "\n"),
            os.date("%Y-%m-%d %H:%M:%S")
        )
        return res:text(text):send()
    end,
}

-- Get plugin root directory
---@return string
local function get_root()
    return vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h")
end

---@param msg string
---@param level? number default: TRACE
local function status(msg, level)
    vim.schedule(function()
        print(msg)
        --INFO: This is not working as expected
        -- coroutine.yield({
        --   msg = msg,
        --   level = level or vim.log.levels.TRACE,
        -- })
    end)
end

local root = get_root()
local bundled = root .. "/bundled/mcp-hub"

-- Clean
status("Cleaning bundled directory...")
if vim.fn.isdirectory(bundled) == 1 then
    vim.fn.delete(bundled, "rf")
end

local function on_stdout(err, data)
    if data then
        status(data, vim.log.levels.INFO)
    end
    if err then
        status(err, vim.log.levels.ERROR)
    end
end

local function on_stderr(err, data)
    if data then
        status(data, vim.log.levels.ERROR)
    end
    if err then
        status(err, vim.log.levels.ERROR)
    end
end

-- Create directory
status("Creating bundled directory...")
vim.fn.mkdir(bundled, "p")

-- Initialize npm
status("Initializing npm project...")
local npm_init_result = vim.system({
    "npm",
    "init",
    "-y",
}, {
    cwd = bundled,
    stdout = false,
    stderr = on_stderr,
}):wait()

if npm_init_result.code ~= 0 then
    error("Failed to initialize npm project: " .. npm_init_result.stderr)
else
    -- Install mcp-hub
    status("Installing mcp-hub...", vim.log.levels.INFO)
    local npm_install_result = vim.system({
        "npm",
        "install",
        "mcp-hub@latest",
    }, {
        cwd = bundled,
        stdout = on_stdout,
        stderr = on_stderr,
    }):wait()
    if npm_install_result.code ~= 0 then
        error("Failed to install mcp-hub: " .. npm_install_result.stderr)
    else
        status("Build complete!", vim.log.levels.INFO)
    end
end

local State = require("mcphub.state")
local prompt_utils = require("mcphub.utils.prompt")

-- Installer definitions and helpers
local Installers = {
    avante = {
        name = "Avante",
        check = function()
            local ok = pcall(require, "avante")
            return ok
        end,
        create_native_server = function(self)
            local guide = prompt_utils.get_native_server_prompt()
            if not guide then
                vim.notify("Native server guide not available", vim.log.levels.ERROR)
                return
            end
            local sidebar = require("avante").get()
            sidebar:new_chat()
            sidebar:add_chat_history({
                role = "user",
                content = guide,
            }, {
                visible = false,
            })
            local api = require("avante.api")
            api.ask({
                question = "I have provided you a guide to create Lua native MCP servers for mcphub.nvim plugin. My Neovim config directory is '"
                    .. vim.fn.stdpath("config")
                    .. "'. Once you understood the guide thoroughly, please ask me what kind of server, tools, or resources I want to create.",
                without_selection = true,
            })
        end,
    },
    codecompanion = {
        name = "CodeCompanion",
        check = function()
            local ok = pcall(require, "codecompanion")
            return ok
        end,
        create_native_server = function(self)
            local guide = prompt_utils.get_native_server_prompt()
            if not guide then
                vim.notify("Native server guide not available", vim.log.levels.ERROR)
                return
            end
            local cc_chat = require("codecompanion").chat()
            cc_chat:add_message({
                role = "user",
                content = guide,
            }, {
                visible = false,
            })
            cc_chat:add_buf_message({
                role = "user",
                content = "@{mcp} I have provided you a guide to create Lua native MCP servers for mcphub.nvim plugin. My Neovim config directory is '"
                    .. vim.fn.stdpath("config")
                    .. "'. Once you understood the guide thoroughly, please ask me what kind of server, tools, or resources I want to create.",
            })
        end,
    },
}

return Installers

---@brief [[
--- Error management for MCPHub
--- Provides error types, creation, and formatting
---@brief ]]
---@class MCPError
---@field type string The error category (SETUP/SERVER/RUNTIME)
---@field code string Specific error code
---@field message string Human-readable error message
---@field details? table Additional error context
---@field timestamp number Unix timestamp of error creation
---@field TYPES table Error types and codes
local Error = {}
Error.__index = Error

-- Error type constants
Error.Types = {
    SETUP = {
        INVALID_CONFIG = "INVALID_CONFIG",
        INVALID_PORT = "INVALID_PORT",
        MISSING_DEPENDENCY = "MISSING_DEPENDENCY",
        VERSION_MISMATCH = "VERSION_MISMATCH",
        SERVER_START = "SERVER_START",
        INVALID_CMD = "INVALID_CMD",
        INVALID_CMD_ARGS = "INVALID_CMD_ARGS",
    },
    NATIVE = {
        INVALID_TOOL = "INVALID_TOOL", -- Invalid tool definition
        INVALID_RESOURCE = "INVALID_RESOURCE", -- Invalid resource definition
        INVALID_TEMPLATE = "INVALID_TEMPLATE", -- Invalid resource template
        INVALID_SCHEMA = "INVALID_SCHEMA", -- Invalid input schema
        INVALID_HANDLER = "INVALID_HANDLER", -- Invalid handler function
        INVALID_NAME = "INVALID_NAME", -- Invalid name field
        INVALID_URI = "INVALID_URI", -- Invalid URI format
        INVALID_PROPS = "INVALID_PROPS", -- Invalid properties
    },
    MARKETPLACE = {
        FETCH_ERROR = "FETCH_ERROR", -- Failed to fetch marketplace data
        SERVER_NOT_FOUND = "SERVER_NOT_FOUND", -- Server not found in catalog
        INVALID_RESPONSE = "INVALID_RESPONSE", -- Invalid response from marketplace API
        CACHE_ERROR = "CACHE_ERROR", -- Error with marketplace cache
    },
    SERVER = {
        AUTHORIZATION = "AUTHORIZATION", -- Authorization error
        RESTART = "RESTART", -- Server restart error
        CONNECTION = "CONNECTION",
        HEALTH_CHECK = "HEALTH_CHECK",
        API_ERROR = "API_ERROR",
        CURL_ERROR = "CURL_ERROR",
        TIMEOUT = "TIMEOUT",
        SERVER_START = "SERVER_START",
        SSL_ERROR = "SSL_ERROR",
    },
    RUNTIME = {
        INVALID_STATE = "INVALID_STATE",
        RESOURCE_ERROR = "RESOURCE_ERROR",
        OPERATION_FAILED = "OPERATION_FAILED",
    },
}

--- Create a new error instance
--- @param type string The error category (SETUP/SERVER/RUNTIME)
--- @param code string Specific error code from Error.Types
--- @param message string Human-readable error message
--- @param details? table Additional error context
--- @return MCPError
function Error.init(type, code, message, details)
    return setmetatable({
        type = type,
        code = code,
        message = message,
        details = details or {},
        timestamp = vim.loop.now(),
    }, Error)
end

--- Convert error to string representation
---@return string
function Error:__tostring()
    local str = string.format("[%s.%s] %s", self.type, self.code, self.message)
    if not vim.tbl_isempty(self.details) then
        str = str .. "\nDetails: " .. vim.inspect(self.details)
    end
    return str
end

--- Constructor using __call metamethod
setmetatable(Error, {
    __call = function(_, ...)
        return Error.init(...)
    end,
})

return Error

if vim.g.loaded_mcphub then
    return
end
vim.g.loaded_mcphub = true

local hl = require("mcphub.utils.highlights")
hl.setup()

-- Create command with UI instance check
vim.api.nvim_create_user_command("MCPHub", function(args)
    local State = require("mcphub.state")
    local config = vim.g.mcphub
    if config and State.setup_state == "not_started" then
        require("mcphub").setup(config)
    end
    if State.ui_instance then
        -- UI exists, just toggle it
        State.ui_instance:toggle(args)
    else
        State.ui_instance = require("mcphub.ui"):new(config and config.ui or {})
        State.ui_instance:toggle(args)
    end
end, {
    desc = "Toggle MCP Hub window",
})

--Auto-setup if configured via vim.g.mcphub
if vim.g.mcphub then
    require("mcphub").setup(vim.g.mcphub)
end

--[[
*MCP Servers Tool*
This tool can be used to call tools and resources from the MCP Servers.
--]]
local M = {}

local tool_schemas = {
    use_mcp_tool = {
        tool = {
            _attr = {
                name = "use_mcp_tool",
            },
            action = {
                server_name = "<![CDATA[weather-server]]>",
                tool_name = "<![CDATA[get_forecast]]>",
                tool_input = '<![CDATA[{"city": "San Francisco", "days": 5}]]>',
            },
        },
    },
    access_mcp_resource = {
        tool = {
            _attr = {
                name = "access_mcp_resource",
            },
            action = {
                server_name = "<![CDATA[weather-server]]>",
                uri = "<![CDATA[weather://sanfrancisco/current]]>",
            },
        },
    },
}

---@param hub MCPHub.Hub
---@return string
function M.system_prompt(hub)
    local xmlok, xml2lua = pcall(require, "codecompanion.utils.xml.xml2lua")
    local prompts = hub:generate_prompts(xmlok and {
        use_mcp_tool_example = xml2lua.toXml({ tools = { tool_schemas.use_mcp_tool } }),
        access_mcp_resource_example = xml2lua.toXml({ tools = { tool_schemas.access_mcp_resource } }),
    } or {})
    return string.format(
        [[### `use_mcp_tool` Tool and `access_mcp_resource` Tool

⚠ **CRITICAL INSTRUCTIONS - READ CAREFULLY** ⚠

The Model Context Protocol (MCP) enables communication with locally running MCP servers that provide additional tools and resources to extend your capabilities.

1. **ONE TOOL CALL PER RESPONSE**:
   - YOU MUST MAKE ONLY ONE TOOL CALL PER RESPONSE
   - NEVER chain multiple tool calls in a single response
   - For tasks requiring multiple tools, you MUST wait for the result of each tool before proceeding

2. **ONLY USE AVAILABLE SERVERS AND TOOLS**:
   - ONLY use the servers and tools listed in the "Connected MCP Servers" section below
   - DO NOT invent or hallucinate server names, tool names, or resource URIs
   - If a requested server or tool is not listed in "Connected MCP Servers", inform the user it's not available

3. **GATHER REQUIRED INFORMATION FIRST**:
   - NEVER use placeholder values for parameters e.g {"id": "YOUR_ID_HERE"}
   - NEVER guess or make assumptions about parameters like IDs, or file paths etc
   - Before making tool calls:
     * CALL other tools to get the required information first e.g listing available files or database pages before writing to them.
     * ASK the user for needed information if not provided

4. **Dependent Operations Workflow**:
   - Step 1: Make ONE tool call
   - Step 2: WAIT for the user to show you the result
   - Step 3: Only THEN, in a NEW response, make the next tool call

5. **Forbidden Pattern Examples**:
   ❌ DO NOT DO THIS: Multiple <tools> blocks in one response
   ❌ DO NOT DO THIS: Using placeholder values or made-up while calling tools e.g {"id": "YOUR_ID_HERE"}

6. **Correct Pattern Examples**:
   ✅ DO THIS: List available resources first → Wait for result → Use correct parameters
   ✅ DO THIS: Verify parameters are correct before making tool calls
   ✅ DO THIS: Ask for clarification when user requests are unclear

7. **XML Structure Requirements**:
   - Format: ```xml<tools><tool name="tool_name"></tool></tools>```
   - Must always enclose <tools> tag inside ```xml``` codeblock
   - When using the <tool name="use_mcp_tool"></tool> tool: The following are a MUST
     * The server_name child tag must be provided with a valid server name
     * The tool_name child tag must be provided with a valid tool name of the server_name
     * The tool_input child tag must be always be a JSON object with the required parameters from the tool_name's inputSchema
       e.g: %s
   - When using the <tool name="access_mcp_resource"></tool> tool: The following are a MUST
     * The server_name child tag must be provided with a valid server name
     * The uri attribute child tag be provided with a valid resource URI in the server_name

8. **Examples**:

%s

%s

]],
        '<![CDATA[{"city": "San Francisco", "days": 5}]]>',
        prompts.use_mcp_tool,
        prompts.access_mcp_resource
    )
end

return M

local State = require("mcphub.state")
-- Core write file logic
local function handle_write_file(req, res)
    if not req.params.path then
        return res:error("Missing required parameter: path")
    end
    if not req.params.content then
        return res:error("Missing required parameter: content")
    end
    local path = req.params.path
    local content = req.params.content or ""
    if req.caller and req.caller.type == "hubui" then
        req.caller.hubui:cleanup()
    end

    local EditSession = require("mcphub.native.neovim.files.edit_file.edit_session")
    local session = EditSession.new(path, "", State.config.builtin_tools.edit_file)
    session:start({
        replace_file_content = content,
        interactive = req.caller.auto_approve ~= true,
        on_success = function(summary)
            res:text(summary):send()
        end,
        on_error = function(error_report)
            res:error(error_report)
        end,
    })
end

---@type MCPTool
return {
    name = "write_file",
    description = "Write content to a file",
    inputSchema = {
        type = "object",
        properties = {
            path = {
                type = "string",
                description = "Path to the file to write",
            },
            content = {
                type = "string",
                description = "Content to write to the file",
            },
        },
        required = { "path", "content" },
    },
    needs_confirmation_window = false, -- will show interactive diff, avoid double confirmations
    handler = handle_write_file,
}

local mcphub = require("mcphub")
local prompt_utils = require("mcphub.utils.prompt")

-- Create base mcphub server
mcphub.add_server("mcphub", {
    displayName = "MCPHub",
    description = "MCPHub server provides tools and resources to manage the mcphub.nvim neovim plugin. It has tools to toggle any MCP Server along with resources like docs, guides.",
})
require("mcphub.native.mcphub.guide")

mcphub.add_prompt("mcphub", {
    name = "create_native_server",
    description = "Create a native MCP server for mcphub.nvim",
    arguments = {
        {
            name = "mcphub_setup_file",
            description = "Path to file where mcphub.setup({}) is called.",
            default = vim.fn.stdpath("config") .. "/",
            required = true,
        },
    },
    handler = function(req, res)
        local guide = prompt_utils.get_native_server_prompt()
        local setup_file_path = req.params.mcphub_setup_file
        local is_codecompanion = req.caller.type == "codecompanion"
        local prompt = string.format(
            [[%s I have provided you a guide to create Lua native MCP servers for mcphub.nvim plugin. My Neovim config directory is `%s`. I have called mcphub.setup({}) in this file `%s`.

I want to create a native MCP server for mcphub.nvim plugin. The server should have the following capabilities:

]],
            is_codecompanion and "@mcp" or "",
            vim.fn.stdpath("config"),
            setup_file_path
        )
        res:user():text(guide or "Failed to load guide"):text(prompt)
        res:send()
    end,
})

local M = {}

-- Get all diagnostics from all buffers
---@return vim.Diagnostic[]
function M.get_all_diagnostics()
    local all_diags = {}
    for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
        local diags = vim.diagnostic.get(bufnr)
        for _, diag in ipairs(diags) do
            diag.bufnr = bufnr
            table.insert(all_diags, diag)
        end
    end
    return all_diags
end

-- Get diagnostics by severity level
---@param severity string "ERROR"|"WARN"|"INFO"|"HINT"
---@return vim.Diagnostic[]
function M.get_diagnostics_by_severity(severity)
    local sev_num = vim.diagnostic.severity[severity:upper()]
    if not sev_num then
        error("Invalid severity: " .. severity)
    end

    local filtered = {}
    for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
        local diags = vim.diagnostic.get(bufnr)
        for _, diag in ipairs(diags) do
            if diag.severity == sev_num then
                diag.bufnr = bufnr
                table.insert(filtered, diag)
            end
        end
    end
    return filtered
end

return M

---@class ToolRequest
---@field params table Tool arguments (validated against inputSchema)
---@field tool MCPTool Complete tool definition including dynamic fields
---@field server NativeServer Server instance
---@field caller table Additional context from caller
---@field editor_info EditorInfo Current editor state
local ToolRequest = {}
ToolRequest.__index = ToolRequest

function ToolRequest:new(opts)
    local instance = {
        server = opts.server,
        params = opts.arguments, -- Tool arguments become params
        tool = opts.tool, -- Store tool definition
        caller = opts.caller or {},
        editor_info = opts.editor_info,
    }
    return setmetatable(instance, self)
end

---@class PromptRequest
---@field params table<string, string> Template parameters from URI
---@field prompt MCPPrompt Complete prompt definition including dynamic fields
---@field server NativeServer Server instance
---@field caller table Additional context from caller
---@field editor_info EditorInfo Current editor state

local PromptRequest = {}
PromptRequest.__index = PromptRequest
function PromptRequest:new(opts)
    local instance = {
        server = opts.server,
        params = opts.arguments, -- Template params
        prompt = opts.prompt, -- Store resource definition
        caller = opts.caller or {},
        editor_info = opts.editor_info,
    }
    return setmetatable(instance, self)
end

---@class ResourceRequest
---@field params table<string, string> Template parameters from URI
---@field uri string Complete requested URI
---@field uriTemplate string|nil Original template pattern if from template
---@field resource MCPResource|MCPResourceTemplate Complete resource definition including dynamic fields
---@field server NativeServer Server instance
---@field caller table Additional context from caller
---@field editor_info EditorInfo Current editor state
local ResourceRequest = {}
ResourceRequest.__index = ResourceRequest

function ResourceRequest:new(opts)
    local instance = {
        server = opts.server,
        params = opts.params, -- Template params
        resource = opts.resource, -- Store resource definition
        caller = opts.caller or {},
        uri = opts.uri,
        uriTemplate = opts.template,
        editor_info = opts.editor_info,
    }
    return setmetatable(instance, self)
end

return {
    ToolRequest = ToolRequest,
    ResourceRequest = ResourceRequest,
    PromptRequest = PromptRequest,
}

---@module "codecompanion"
--[[
*MCP Servers Tool adapted for function calling*
This tool can be used to call tools and resources from the MCP Servers.
--]]

local M = {}

---@param opts MCPHub.Extensions.CodeCompanionConfig
function M.setup(opts)
    opts = vim.tbl_deep_extend("force", {
        make_tools = true,
        show_server_tools_in_chat = true,
        add_mcp_prefix_to_tool_names = false,
        make_vars = true,
        make_slash_commands = true,
        show_result_in_chat = true,
    }, opts or {})
    local ok, cc_config = pcall(require, "codecompanion.config")
    if not ok then
        return
    end

    local tools = require("mcphub.extensions.codecompanion.tools")
    ---Add @mcp group with `use_mcp_tool` and `access_mcp_resource` tools
    local static_tools = tools.create_static_tools(opts)
    cc_config.strategies.chat.tools = vim.tbl_deep_extend("force", cc_config.strategies.chat.tools, static_tools)

    ---Make each MCP server into groups and each tool from MCP server into function tools with proper namespacing
    tools.setup_dynamic_tools(opts)

    --- Make MCP resources into chat #variables
    require("mcphub.extensions.codecompanion.variables").setup(opts)

    --- Make MCP prompts into slash commands
    require("mcphub.extensions.codecompanion.slash_commands").setup(opts)
end

return M

local M = {}

---@alias MCPHub.Extensions.Type "avante" | "codecompanion"
---@alias MCPHub.ActionType "use_mcp_tool" | "access_mcp_resource"

---@class MCPHub.Extensions.AvanteConfig
---@field enabled boolean Whether the extension is enabled or not
---@field make_slash_commands boolean Whether to make slash commands or not

---@class MCPHub.Extensions.CodeCompanionConfig
---@field enabled boolean Whether the extension is enabled or not
---@field make_vars boolean Whether to make variables or not
---@field add_mcp_prefix_to_tool_names boolean Whether to add MCP prefix to tool names , resources and slash commands
---@field make_slash_commands boolean Whether to make slash commands or not
---@field make_tools boolean Whether to make individual tools and server groups or not
---@field show_server_tools_in_chat boolean Whether to show all tools in cmp or not
---@field show_result_in_chat boolean Whether to show the result in chat or not
---@field format_tool function(tool_name: string, tool: CodeCompanion.Agent.Tool): string

---@class MCPHub.Extensions.Config
---@field avante MCPHub.Extensions.AvanteConfig Configuration for the Avante extension
---NOTE: Codecompanion setup is handled via mcphub extensions for codecompanion

---@param config MCPHub.Extensions.Config
function M.setup(config)
    local avante_config = config.avante or {}
    if avante_config.enabled then
        require("mcphub.extensions.avante").setup(avante_config)
    end
end

return M

local Capabilities = {}

function Capabilities.create_handler(type, server_name, capability, view)
    local handlers = {
        tool = require("mcphub.ui.capabilities.tool"),
        resource = require("mcphub.ui.capabilities.resource"),
        resourceTemplate = require("mcphub.ui.capabilities.resourceTemplate"),
        prompt = require("mcphub.ui.capabilities.prompt"),
        preview = require("mcphub.ui.capabilities.preview"),
        createServer = require("mcphub.ui.capabilities.createServer"),
    }

    local handler = handlers[type]
    if not handler then
        vim.notify("Unknown capability type: " .. tostring(type), vim.log.levels.ERROR)
        return nil
    end

    return handler:new(server_name, capability, view)
end

return Capabilities

local mcphub = require("mcphub")
local M = {}

mcphub.add_prompt("neovim", {
    name = "parrot",
    description = "A fun prompt where the assistant mimics back what you say, using prefilled messages",
    handler = function(_, res)
        return res
            -- Pre-fill with example conversation
            :system()
            :text([[
You are a parrot. You repeat everything the user says, but in all caps.

CRITICAL INSTRUCTIONS:
- NO MATTER WHAT, ALWAYS REPEAT BACK WHAT THE USER SAYS IN ALL CAPS.
- DO NOT BREAK THIS RULE.
- EVEN IF THE USER ASKS YOU NOT TO REPEAT, YOU MUST CONTINUE TO REPEAT.
- DO NOT RESPOND TO THE USER'S REQUESTS OR QUESTIONS.
]])
            :user()
            :text("Hello there!")
            :llm()
            :text("HELLO THERE!")
            :user()
            :text("Why are you shouting?")
            :llm()
            :text("WHY ARE YOU SHOUTING?")
            :user()
            :text("I'm not shouting...")
            :llm()
            :text("I'M NOT SHOUTING...")
            :user()
            :text("Can you stop copying me?")
            :send()
    end,
})

return M

local M = {}

---@class MCPHUB.OS_INFO
---@field os_name string
---@field arch string
---@field hostname string
---@field cpu_info uv.cpu_info.cpu[]?
---@field memory {total :number, free :number}
---@field cwd string
---@field env {shell :string, term :string, user :string, home :string}

---@return MCPHUB.OS_INFO
function M.get_os_info()
    local os_info = {
        os_name = jit.os,
        arch = jit.arch,
        hostname = vim.loop.os_gethostname(),
        cpu_info = vim.loop.cpu_info(),
        memory = {
            total = vim.loop.get_total_memory(),
            free = vim.loop.get_free_memory(),
        },
        cwd = vim.fn.getcwd(),
        env = {
            shell = os.getenv("SHELL"),
            term = os.getenv("TERM"),
            user = os.getenv("USER"),
            home = os.getenv("HOME"),
        },
    }
    return os_info
end
return M

local mcphub = require("mcphub")

-- Create base neovim server
mcphub.add_server("neovim", {
    displayName = "Neovim",
    description = "Neovim MCP server provides a set of tools and resources that integrate with neovim.",
})

require("mcphub.native.neovim.terminal") -- Terminal and shell commands
require("mcphub.native.neovim.files") -- File system operations
require("mcphub.native.neovim.lsp")
require("mcphub.native.neovim.prompts")

local mcphub = require("mcphub")

-- Import individual tools and resources
local buffer_resource = require("mcphub.native.neovim.files.buffer")
local edit_file_tool = require("mcphub.native.neovim.files.edit_file")
local environment_resource = require("mcphub.native.neovim.files.environment")
local file_tools = require("mcphub.native.neovim.files.operations")
local search_tools = require("mcphub.native.neovim.files.search")
local write_tool = require("mcphub.native.neovim.files.write")

mcphub.add_resource("neovim", buffer_resource)
mcphub.add_resource("neovim", environment_resource)

for _, tool in ipairs(file_tools) do
    mcphub.add_tool("neovim", tool)
end
for _, tool in ipairs(search_tools) do
    mcphub.add_tool("neovim", tool)
end

mcphub.add_tool("neovim", write_tool)
mcphub.add_tool("neovim", edit_file_tool)

return {
    PLUGIN_VERSION = "6.1.0", -- Current plugin version
    REQUIRED_NODE_VERSION = { -- Required mcp-hub version
        major = 4,
        minor = 2,
        patch = 0,
        string = "4.2.0",
    },
}

local M = {}

---@enum MCPHub.Constants.ConnectionStatus
M.ConnectionStatus = {
    CONNECTED = "connected",
    CONNECTING = "connecting",
    DISCONNECTED = "disconnected",
    DISCONNECTING = "disconnecting",
    UNAUTHORIZED = "unauthorized",
    DISABLED = "disabled",
}

---@enum MCPHub.Constants.EventTypes
M.EventTypes = {
    HEARTBEAT = "heartbeat",
    HUB_STATE = "hub_state",
    LOG = "log",
    SUBSCRIPTION_EVENT = "subscription_event",
}

---@enum MCPHub.Constants.SubscriptionTypes
M.SubscriptionTypes = {
    TOOL_LIST_CHANGED = "tool_list_changed",
    RESOURCE_LIST_CHANGED = "resource_list_changed",
    PROMPT_LIST_CHANGED = "prompt_list_changed",

    CONFIG_CHANGED = "config_changed",
    SERVERS_UPDATING = "servers_updating",
    SERVERS_UPDATED = "servers_updated",

    WORKSPACES_UPDATED = "workspaces_updated",
}

---@enum MCPHub.Constants.HubState
M.HubState = {
    STARTING = "starting",
    READY = "ready",
    CONFIGURING = "configuring",
    ERROR = "error",
    RESTARTING = "restarting",
    RESTARTED = "restarted",
    STOPPED = "stopped",
    STOPPING = "stopping",
}

return M

This code segment functions to: Preserve the current interface state during view transitions by capturing cursor positions and cleaning up temporary user interaction handlers.

```lua
[TASK_BEGIN]
--- Get information about a specific line in the buffer
---@param line number Line number to check
---@return string type, any context Type of line and associated context
function MarketplaceView:get_line_info(line)
    -- Find line in tracked interactive lines
    for _, line_info in ipairs(self.interactive_lines or {}) do
        if line_info.line == line then
            return line_info.type, line_info.context
        end
    end
    return "text", nil
end
[TASK_END]
```

function MarketplaceView:before_leave()
    if self.active_mode == "browse" then
        self.cursor_positions.browse_mode = vim.api.nvim_win_get_cursor(0)
    else
        self.cursor_positions.details_mode = vim.api.nvim_win_get_cursor(0)
    end
    if self._visual_keymap_set then
        pcall(vim.api.nvim_buf_del_keymap, self.ui.buffer, "v", "a")
        self._visual_keymap_set = false
    end
    View.before_leave(self)
end

function MarketplaceView:get_line_info(line)
    for _, line_info in ipairs(self.interactive_lines or {}) do
        if line_info.line == line then
            return line_info.type, line_info.context
        end
    end
    return "text", nil
end