"""Interactive plugin screen for /plugins (/skills) command.

Three tabs: Installed skills, Store (available from registries),
and Connectors (MCP server management).
"""

from __future__ import annotations

import asyncio
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar, Literal

from textual.binding import Binding, BindingType
from textual.containers import Container, Vertical, VerticalScroll
from textual.events import Click
from textual.message import Message
from textual.screen import ModalScreen
from textual.widgets import Input, Static, TabbedContent, TabPane

if TYPE_CHECKING:
    from textual.app import ComposeResult

from icarus.config.display import CharsetMode, _detect_charset_mode, get_glyphs
from icarus_tui.widgets.navigable_list import NavigableListMixin

logger = logging.getLogger(__name__)


@dataclass
class SkillItem:
    """A single item in the skill store list."""

    name: str
    description: str
    installed: bool
    source: str  # registry name, "built-in", "user", "project", etc.
    kind: Literal["action", "header", "installed", "local", "available"]


@dataclass
class ConnectorItem:
    """A single MCP connector in the connectors tab."""

    name: str
    transport_type: str  # "stdio" or "http"
    enabled: bool
    status: str  # "connected", "error", "disabled"
    error: str  # error message if status == "error"
    tools: list[str]
    kind: Literal["action", "connector"]


# ---------------------------------------------------------------------------
# Generic list item widget
# ---------------------------------------------------------------------------


class ListOption(Static):
    """A clickable row in a navigable list."""

    def __init__(
        self,
        label: str,
        item_name: str,
        index: int,
        *,
        classes: str = "",
    ) -> None:
        super().__init__(label, classes=classes)
        self.item_name = item_name
        self.index = index

    class Clicked(Message):
        def __init__(self, item_name: str, index: int) -> None:
            super().__init__()
            self.item_name = item_name
            self.index = index

    def on_click(self, event: Click) -> None:
        event.stop()
        self.post_message(self.Clicked(self.item_name, self.index))


# ---------------------------------------------------------------------------
# Add Registry dialog
# ---------------------------------------------------------------------------


class AddRegistryScreen(ModalScreen[tuple[str, str] | None]):
    """Modal dialog for adding a new skill registry."""

    CSS = """
    AddRegistryScreen {
        align: center middle;
    }

    AddRegistryScreen > Vertical {
        width: 70;
        max-width: 90%;
        height: auto;
        max-height: 22;
        background: $surface;
        border: solid $primary;
        padding: 1 2;
    }

    AddRegistryScreen .add-reg-title {
        text-style: bold;
        color: $primary;
        text-align: center;
        margin-bottom: 1;
    }

    AddRegistryScreen .add-reg-examples {
        color: $text-muted;
        margin-bottom: 1;
    }

    AddRegistryScreen .add-reg-token-label {
        color: $text-muted;
        margin-top: 1;
    }

    AddRegistryScreen .add-reg-help {
        height: 1;
        color: $text-muted;
        text-style: italic;
        text-align: center;
    }
    """

    BINDINGS: ClassVar[list[BindingType]] = [
        Binding("escape", "cancel", "Cancel", show=False, priority=True),
    ]

    def compose(self) -> ComposeResult:
        with Vertical():
            yield Static("Add Marketplace", classes="add-reg-title")
            yield Static(
                "Enter marketplace source:\n"
                "Examples:\n"
                "  [dim]owner/repo[/dim] (GitHub)\n"
                "  [dim]github:owner/repo[/dim]",
                classes="add-reg-examples",
            )
            yield Input(placeholder="owner/repo", id="registry-input")
            yield Static(
                "[dim]Token (optional — for private repos):[/dim]",
                classes="add-reg-token-label",
            )
            yield Input(
                placeholder="ghp_... or leave empty",
                password=True,
                id="registry-token",
            )
            yield Static(
                "[dim italic]Enter to add · Tab to switch fields · Esc to cancel[/dim italic]",
                classes="add-reg-help",
            )

    async def on_mount(self) -> None:
        if _detect_charset_mode() == CharsetMode.ASCII:
            self.query_one(Vertical).styles.border = ("ascii", "green")
        self.query_one("#registry-input", Input).focus()

    def on_input_submitted(self, event: Input.Submitted) -> None:
        event.stop()
        source = self.query_one("#registry-input", Input).value.strip()
        if source:
            token = self.query_one("#registry-token", Input).value.strip()
            self.dismiss((source, token))

    def action_cancel(self) -> None:
        self.dismiss(None)


# ---------------------------------------------------------------------------
# Add Connector dialog
# ---------------------------------------------------------------------------


class AddConnectorScreen(ModalScreen[tuple[str, str] | None]):
    """Modal dialog for adding a new MCP connector.

    Returns (name, config_json) or None if cancelled.
    """

    CSS = """
    AddConnectorScreen {
        align: center middle;
    }

    AddConnectorScreen > Vertical {
        width: 70;
        max-width: 90%;
        height: auto;
        max-height: 28;
        background: $surface;
        border: solid $primary;
        padding: 1 2;
    }

    AddConnectorScreen .add-conn-title {
        text-style: bold;
        color: $primary;
        text-align: center;
        margin-bottom: 1;
    }

    AddConnectorScreen .add-conn-examples {
        color: $text-muted;
        margin-bottom: 1;
    }

    AddConnectorScreen .add-conn-help {
        height: 1;
        color: $text-muted;
        text-style: italic;
        text-align: center;
    }

    AddConnectorScreen .add-conn-label {
        color: $text-muted;
        margin-top: 1;
    }
    """

    BINDINGS: ClassVar[list[BindingType]] = [
        Binding("escape", "cancel", "Cancel", show=False, priority=True),
    ]

    def compose(self) -> ComposeResult:
        with Vertical():
            yield Static("Add Connector", classes="add-conn-title")
            yield Static(
                "Add a local (stdio) or remote (HTTP) MCP server.\n"
                "For stdio, enter the command as JSON config.",
                classes="add-conn-examples",
            )
            yield Static("[dim]Name:[/dim]", classes="add-conn-label")
            yield Input(placeholder="e.g. playwright", id="conn-name")
            yield Static(
                "[dim]Config JSON:[/dim]",
                classes="add-conn-label",
            )
            yield Input(
                placeholder='{"command": "npx", "args": ["@playwright/mcp@latest"]}',
                id="conn-config",
            )
            yield Static(
                "[dim italic]Enter to add · Tab to switch fields · Esc to cancel[/dim italic]",
                classes="add-conn-help",
            )

    async def on_mount(self) -> None:
        if _detect_charset_mode() == CharsetMode.ASCII:
            self.query_one(Vertical).styles.border = ("ascii", "green")
        self.query_one("#conn-name", Input).focus()

    def on_input_submitted(self, event: Input.Submitted) -> None:
        event.stop()
        name = self.query_one("#conn-name", Input).value.strip()
        config_json = self.query_one("#conn-config", Input).value.strip()
        if name and config_json:
            self.dismiss((name, config_json))

    def action_cancel(self) -> None:
        self.dismiss(None)


# ---------------------------------------------------------------------------
# Main plugin screen
# ---------------------------------------------------------------------------


class SkillStoreScreen(NavigableListMixin, ModalScreen[str | None]):
    """Modal screen for browsing and managing skills.

    Shows installed skills and available skills from registries in tabs.
    """

    _nav_selected_css_class = "skill-option-selected"

    BINDINGS: ClassVar[list[BindingType]] = [
        Binding("up", "move_up", "Up", show=False, priority=True),
        Binding("k", "move_up", "Up", show=False, priority=True),
        Binding("down", "move_down", "Down", show=False, priority=True),
        Binding("j", "move_down", "Down", show=False, priority=True),
        Binding("pageup", "page_up", "Page up", show=False, priority=True),
        Binding("pagedown", "page_down", "Page down", show=False, priority=True),
        Binding("enter", "select_item", "Select", show=False, priority=True),
        Binding("delete", "remove_item", "Remove", show=False, priority=True),
        Binding("f5", "refresh_registries", "Refresh", show=False, priority=True),
        Binding("escape", "cancel", "Cancel", show=False, priority=True),
    ]

    CSS = """
    SkillStoreScreen {
        align: center middle;
    }

    SkillStoreScreen > Vertical {
        width: 80;
        max-width: 90%;
        height: 80%;
        background: $surface;
        border: solid $primary;
        padding: 1 2;
    }

    SkillStoreScreen .skill-store-title {
        text-style: bold;
        color: $primary;
        text-align: center;
        margin-bottom: 1;
    }

    SkillStoreScreen #skill-filter {
        margin-bottom: 1;
        border: solid $primary-lighten-2;
    }

    SkillStoreScreen #skill-filter:focus {
        border: solid $primary;
    }

    SkillStoreScreen #skill-tabs {
        height: 1fr;
    }

    SkillStoreScreen .skill-list {
        height: 1fr;
        min-height: 5;
        scrollbar-gutter: stable;
        background: $background;
    }

    SkillStoreScreen .tab-options {
        height: auto;
    }

    SkillStoreScreen .skill-option {
        height: 1;
        padding: 0 1;
    }

    SkillStoreScreen .skill-option:hover {
        background: $surface-lighten-1;
    }

    SkillStoreScreen .skill-option-selected {
        background: $primary;
        text-style: bold;
    }

    SkillStoreScreen .skill-option-selected:hover {
        background: $primary-lighten-1;
    }

    SkillStoreScreen .skill-registry-header {
        height: 1;
        padding: 0 1;
        color: $primary;
        margin-top: 1;
    }

    SkillStoreScreen .skill-store-help {
        height: 1;
        color: $text-muted;
        text-style: italic;
        margin-top: 1;
        text-align: center;
    }

    SkillStoreScreen .skill-store-status {
        height: 1;
        color: $text-muted;
        text-align: center;
    }
    """

    def __init__(self) -> None:
        super().__init__()
        self._installed_items: list[SkillItem] = []
        self._store_items: list[SkillItem] = []
        self._connector_items: list[ConnectorItem] = []
        self._filtered_items: list[SkillItem | ConnectorItem] = []
        self._selected_index = 0
        self._option_widgets: list[ListOption] = []
        self._selectable_indices: list[int] = []
        self._filter_text = ""
        self._active_tab = "tab-installed"

    # ── Layout ────────────────────────────────────────────────────────

    def compose(self) -> ComposeResult:
        glyphs = get_glyphs()

        with Vertical():
            yield Static("Plugins", classes="skill-store-title")

            yield Input(
                placeholder="Type to filter...",
                id="skill-filter",
            )

            with TabbedContent(id="skill-tabs"):
                with TabPane("Installed", id="tab-installed"):
                    with VerticalScroll(classes="skill-list"):
                        yield Container(
                            id="installed-options", classes="tab-options",
                        )
                with TabPane("Store", id="tab-store"):
                    with VerticalScroll(classes="skill-list"):
                        yield Container(
                            id="store-options", classes="tab-options",
                        )
                with TabPane("Connectors", id="tab-connectors"):
                    with VerticalScroll(classes="skill-list"):
                        yield Container(
                            id="connector-options", classes="tab-options",
                        )

            yield Static(
                "[dim]Loading...[/dim]",
                classes="skill-store-status",
                id="skill-status",
            )

            help_text = (
                f"{glyphs.arrow_up}/{glyphs.arrow_down} navigate "
                f"{glyphs.bullet} Enter select "
                f"{glyphs.bullet} Del remove "
                f"{glyphs.bullet} F5 refresh "
                f"{glyphs.bullet} Esc close"
            )
            yield Static(help_text, classes="skill-store-help")

    async def on_mount(self) -> None:
        if _detect_charset_mode() == CharsetMode.ASCII:
            self.query_one(Vertical).styles.border = ("ascii", "green")

        self.run_worker(self._load_skills, exclusive=True)
        self.query_one("#skill-filter", Input).focus()

    # ── Tab switching ─────────────────────────────────────────────────

    def on_tabbed_content_tab_activated(
        self, event: TabbedContent.TabActivated,
    ) -> None:
        self._active_tab = event.pane.id
        self._selected_index = 0
        self._update_filtered_list()
        self.call_after_refresh(self._update_active_display)
        self.query_one("#skill-filter", Input).focus()

    # ── Data loading ──────────────────────────────────────────────────

    async def _load_skills(self) -> None:
        """Load installed skills and registry index."""
        installed_items: list[SkillItem] = []
        store_items: list[SkillItem] = []
        installed_names: set[str] = set()

        # 1. Registry-installed skills
        try:
            from icarus.skills.installer import list_installed

            installed = await asyncio.to_thread(list_installed)
            installed_names = {s.name for s in installed}
            for s in installed:
                installed_items.append(SkillItem(
                    s.name, f"v{s.version} from {s.registry}",
                    True, s.registry, "installed",
                ))
        except Exception:
            logger.exception("Failed to load installed skills")

        # 2. Local skills (built-in + user + project)
        try:
            from icarus.config.app_config import Settings
            from icarus.skills.load import list_skills

            settings = Settings.from_environment()
            local_skills = await asyncio.to_thread(
                list_skills,
                built_in_skills_dir=settings.get_built_in_skills_dir(),
                user_skills_dir=settings.get_user_skills_dir("default"),
                project_skills_dir=settings.get_project_skills_dir(),
                plugins_base_dir=settings.get_plugins_base_dir(),
            )
            for s in local_skills:
                name = s["name"]
                if name not in installed_names:
                    desc = s.get("description", "")[:60]
                    source = s.get("source", "local")
                    installed_items.append(SkillItem(name, desc, True, source, "local"))
                    installed_names.add(name)
        except Exception:
            logger.exception("Failed to load local skills")

        # 3. "Add marketplace..." action (always first in Store tab)
        store_items.append(SkillItem(
            "", "Add new skill marketplace...", False, "", "action",
        ))

        # 4. Available skills from registries, grouped by registry
        try:
            from icarus.skills.registry import fetch_index
            from icarus.skills.registry_config import SkillsConfig

            config = await asyncio.to_thread(SkillsConfig.load)

            # Fetch all registries concurrently
            indexes = await asyncio.gather(
                *(asyncio.to_thread(fetch_index, e) for e in config.registries),
                return_exceptions=True,
            )

            for entry, index in zip(config.registries, indexes):
                if isinstance(index, BaseException):
                    logger.warning("Failed to fetch %s: %s", entry.name, index)
                    index = []
                source_label = entry.url.removeprefix("github:")
                header_desc = f"{entry.name} ({source_label})"
                store_items.append(SkillItem(
                    "", header_desc, False, entry.name, "header",
                ))
                for s in index:
                    if s.name not in installed_names:
                        tags = (
                            f" [{', '.join(s.tags)}]" if s.tags else ""
                        )
                        store_items.append(SkillItem(
                            s.name,
                            f"{s.description[:50]}{tags}",
                            False,
                            entry.name,
                            "available",
                        ))
        except Exception:
            logger.exception("Failed to load registry index")

        # 5. MCP connectors
        connector_items: list[ConnectorItem] = []
        connector_items.append(ConnectorItem(
            name="Add new connector...", transport_type="", enabled=False,
            status="", error="", tools=[], kind="action",
        ))
        try:
            from icarus.mcp.config import list_servers, read_mcp_config

            config_path = self._mcp_config_path()
            config = await asyncio.to_thread(read_mcp_config, config_path)
            for entry in list_servers(config):
                connector_items.append(ConnectorItem(
                    name=entry.name,
                    transport_type=entry.transport_type,
                    enabled=entry.enabled,
                    status="disabled" if not entry.enabled else "configured",
                    error="",
                    tools=[],
                    kind="connector",
                ))
        except Exception:
            logger.exception("Failed to load MCP connectors")

        self._installed_items = installed_items
        self._store_items = store_items
        self._connector_items = connector_items
        self._update_filtered_list()
        self._on_skills_loaded()

    def _on_skills_loaded(self) -> None:
        n_installed = len(self._installed_items)
        n_available = sum(
            1 for i in self._store_items if i.kind == "available"
        )
        n_connectors = sum(
            1 for i in self._connector_items if i.kind == "connector"
        )

        status = self.query_one("#skill-status", Static)
        status.update(
            f"[dim]{n_installed} skills, {n_available} available, "
            f"{n_connectors} connectors[/dim]"
        )
        self.call_after_refresh(self._update_active_display)

    # ── Filtering ─────────────────────────────────────────────────────

    def _get_active_items(self) -> list[SkillItem] | list[ConnectorItem]:
        if self._active_tab == "tab-installed":
            return self._installed_items
        if self._active_tab == "tab-connectors":
            return self._connector_items
        return self._store_items

    def _get_active_container_id(self) -> str:
        if self._active_tab == "tab-installed":
            return "installed-options"
        if self._active_tab == "tab-connectors":
            return "connector-options"
        return "store-options"

    def _update_filtered_list(self) -> None:
        items = self._get_active_items()
        if not self._filter_text:
            self._filtered_items = list(items)
        else:
            filtered = []
            for item in items:
                if isinstance(item, ConnectorItem):
                    if (item.kind == "action"
                            or self._filter_text in item.name.lower()):
                        filtered.append(item)
                elif (item.kind in ("action", "header")
                      or self._filter_text in item.name.lower()
                      or self._filter_text in item.description.lower()):
                    filtered.append(item)
            self._filtered_items = filtered
        selectable_count = len(self._filtered_items)
        if self._selected_index >= selectable_count:
            self._selected_index = max(0, selectable_count - 1)

    def on_input_changed(self, event: Input.Changed) -> None:
        self._filter_text = event.value.lower()
        self._selected_index = 0
        self._update_filtered_list()
        self.call_after_refresh(self._update_active_display)

    def on_input_submitted(self, event: Input.Submitted) -> None:
        event.stop()
        self.action_select_item()

    def on_list_option_clicked(self, event: ListOption.Clicked) -> None:
        if self._option_widgets and event.index != self._selected_index:
            self._move_selection(event.index - self._selected_index)

    # ── Display ───────────────────────────────────────────────────────

    async def _update_active_display(self) -> None:
        container_id = self._get_active_container_id()
        try:
            container = self.query_one(f"#{container_id}", Container)
        except Exception:  # noqa: BLE001
            return

        await container.remove_children()
        self._option_widgets = []
        # Map from selectable index → filtered_items index
        self._selectable_indices: list[int] = []

        if not self._filtered_items:
            await container.mount(Static("[dim]No matching skills[/dim]"))
            return

        all_widgets: list[Static] = []
        sel_idx = 0

        for fi_idx, item in enumerate(self._filtered_items):
            is_selected = sel_idx == self._selected_index

            if item.kind == "header":
                classes = "skill-registry-header"
                if is_selected:
                    classes += " skill-option-selected"
            else:
                classes = "skill-option"
                if is_selected:
                    classes += " skill-option-selected"

            label = self._format_label(item, selected=is_selected)
            widget = ListOption(
                label=label,
                item_name=item.name,
                index=sel_idx,
                classes=classes,
            )
            all_widgets.append(widget)
            self._option_widgets.append(widget)
            self._selectable_indices.append(fi_idx)
            sel_idx += 1

        await container.mount(*all_widgets)

        # Scroll selected into view
        if self._option_widgets and self._selected_index < len(
            self._option_widgets,
        ):
            w = self._option_widgets[self._selected_index]
            if self._selected_index == 0:
                parent = container.parent
                if isinstance(parent, VerticalScroll):
                    parent.scroll_home(animate=False)
            else:
                w.scroll_visible(animate=False)

    @staticmethod
    def _format_label(item: SkillItem | ConnectorItem, *, selected: bool) -> str:
        glyphs = get_glyphs()
        cursor = f"{glyphs.cursor} " if selected else "  "

        if item.kind == "action":
            desc = item.name if isinstance(item, ConnectorItem) else item.description
            return f"{cursor}[bold]+ {desc}[/bold]"

        if isinstance(item, ConnectorItem):
            return SkillStoreScreen._format_connector_label(item, cursor=cursor, selected=selected)

        if item.kind == "header":
            hint = " [dim](Del to remove)[/dim]" if selected else ""
            return f"{cursor}[bold]{item.description}[/bold]{hint}"

        status = f"[green]{glyphs.checkmark}[/green]" if item.installed else " "
        max_desc = 45
        desc = item.description
        short_desc = desc[:max_desc] + "..." if len(desc) > max_desc else desc
        return f"{cursor}{status} {item.name:<20} [dim]{short_desc}[/dim]"

    @staticmethod
    def _format_connector_label(item: ConnectorItem, *, cursor: str, selected: bool) -> str:
        if item.enabled:
            dot = "[green]●[/green]"
            state = "[green]ON[/green] "
        else:
            dot = "[dim]○[/dim]"
            state = "[dim]OFF[/dim]"
        transport = f"[dim]{item.transport_type}[/dim]" if item.transport_type else ""
        hint = ""
        if selected:
            if item.enabled:
                hint = " [yellow](Enter=disable · Del=remove)[/yellow]"
            else:
                hint = " [yellow](Enter=enable · Del=remove)[/yellow]"
        return f"{cursor}{dot} {item.name:<20} {state} {transport}{hint}"

    # ── Navigation (hooks for NavigableListMixin) ────────────────────

    def _get_selectable_item(self, sel_idx: int) -> SkillItem | ConnectorItem | None:
        """Get the item for a selectable widget index."""
        if 0 <= sel_idx < len(self._selectable_indices):
            fi_idx = self._selectable_indices[sel_idx]
            return self._filtered_items[fi_idx]
        return None

    def _nav_get_scroll(self) -> VerticalScroll | None:
        cid = self._get_active_container_id()
        try:
            parent = self.query_one(f"#{cid}", Container).parent
            if isinstance(parent, VerticalScroll):
                return parent
        except Exception:  # noqa: BLE001
            pass
        return None

    def _nav_render_widget(self, index: int, *, selected: bool) -> None:
        item = self._get_selectable_item(index)
        if item:
            self._option_widgets[index].update(
                self._format_label(item, selected=selected),
            )

    # ── Actions ───────────────────────────────────────────────────────

    async def action_select_item(self) -> None:
        """Handle Enter on the selected item."""
        item = self._get_selectable_item(self._selected_index)
        if not item:
            return

        if isinstance(item, ConnectorItem):
            if item.kind == "action":
                self._open_add_connector()
            elif item.kind == "connector":
                await self._toggle_connector(item.name)
            return

        if item.kind == "action":
            self._open_add_registry()
        elif item.kind == "header":
            pass
        elif item.kind == "installed":
            self.run_worker(
                lambda: self._do_uninstall(item.name), exclusive=True, thread=True,
            )
        elif item.kind == "available":
            self.run_worker(
                lambda: self._do_install(item.name), exclusive=True, thread=True,
            )
        else:
            self._show_status(
                f"[yellow]{item.name} is a local skill"
                " — manage it from the filesystem[/yellow]",
            )

    async def action_remove_item(self) -> None:
        """Handle Delete — remove a registry, uninstall a skill, or remove a connector."""
        item = self._get_selectable_item(self._selected_index)
        if not item:
            return

        if isinstance(item, ConnectorItem) and item.kind == "connector":
            await self._remove_connector(item.name)
            return

        if item.kind == "header":
            await self._remove_registry(item.source)
        elif item.kind == "installed":
            self.run_worker(
                lambda: self._do_uninstall(item.name), exclusive=True, thread=True,
            )

    def action_refresh_registries(self) -> None:
        """Handle F5 — refresh all registries."""
        self._show_status("[dim]Refreshing...[/dim]")
        self._reload()

    def _open_add_registry(self) -> None:
        async def handle_result(result: tuple[str, str] | None) -> None:
            if result:
                source, token = result
                await self._add_registry(source, token)
            self.query_one("#skill-filter", Input).focus()

        self.app.push_screen(AddRegistryScreen(), handle_result)

    async def _add_registry(self, source: str, token: str = "") -> None:
        """Parse source string and add as a new registry."""
        try:
            from icarus.skills.registry_config import RegistryEntry, SkillsConfig

            source = source.strip()
            if "/" in source and not source.startswith("github:"):
                url = f"github:{source}"
            else:
                url = source

            # Derive name from repo portion
            name = url.split("/")[-1] if "/" in url else source

            # Save token to ~/.icarus/.env if provided
            token_env = ""
            if token:
                from icarus.config.model_config import save_credentials

                token_env = f"REGISTRY_TOKEN_{name.upper().replace('-', '_')}"
                await asyncio.to_thread(save_credentials, {token_env: token})

            config = await asyncio.to_thread(SkillsConfig.load)
            config.add_registry(RegistryEntry(name=name, url=url, token_env=token_env))
            await asyncio.to_thread(config.save)

            self._show_status(f"[green]Added registry: {name}[/green]")
            self._reload()
        except Exception as e:
            logger.exception("Failed to add registry")
            self._show_status(f"[red]Failed to add registry: {e}[/red]")

    async def _remove_registry(self, registry_name: str) -> None:
        """Remove a registry from the config."""
        try:
            from icarus.skills.registry_config import SkillsConfig

            config = await asyncio.to_thread(SkillsConfig.load)
            config.remove_registry(registry_name)
            await asyncio.to_thread(config.save)

            self._show_status(f"[green]Removed registry: {registry_name}[/green]")
            self._reload()
        except Exception as e:
            logger.exception("Failed to remove registry %s", registry_name)
            self._show_status(f"[red]Failed to remove: {e}[/red]")

    def _do_install(self, name: str) -> None:
        """Install a skill from the registry (runs in worker thread)."""
        try:
            from icarus.skills.installer import install_skill

            result = install_skill(name)
            self.app.call_from_thread(
                self._show_status,
                f"[green]Installed {result.name} v{result.version}[/green]",
            )
            self.app.call_from_thread(self._reload)
        except Exception as e:
            logger.exception("Failed to install skill %s", name)
            self.app.call_from_thread(
                self._show_status,
                f"[red]Failed to install {name}: {e}[/red]",
            )

    def _do_uninstall(self, name: str) -> None:
        """Uninstall a registry-installed skill (runs in worker thread)."""
        try:
            from icarus.skills.installer import uninstall_skill

            uninstall_skill(name)
            self.app.call_from_thread(
                self._show_status,
                f"[green]Uninstalled {name}[/green]",
            )
            self.app.call_from_thread(self._reload)
        except Exception as e:
            logger.exception("Failed to uninstall skill %s", name)
            self.app.call_from_thread(
                self._show_status,
                f"[red]Failed to uninstall {name}: {e}[/red]",
            )

    # ── Connector management ─────────────────────────────────────────

    def _open_add_connector(self) -> None:
        async def handle_result(result: tuple[str, str] | None) -> None:
            if result:
                name, config_json = result
                await self._add_connector(name, config_json)
            self.query_one("#skill-filter", Input).focus()

        self.app.push_screen(AddConnectorScreen(), handle_result)

    @staticmethod
    def _mcp_config_path() -> Path:
        return Path.home() / ".icarus" / "mcp.json"

    async def _mutate_mcp_config(self, action: str, mutate_fn, name: str, *args) -> None:
        """Read config, apply mutation, write, show status, reload."""
        from icarus.mcp.config import read_mcp_config, write_mcp_config

        config_path = self._mcp_config_path()
        config = await asyncio.to_thread(read_mcp_config, config_path)
        mutate_fn(config, name, *args)
        await asyncio.to_thread(write_mcp_config, config_path, config)

        self._show_status(f"[green]{action} connector: {name}[/green]")
        self._reload()

    async def _add_connector(self, name: str, config_json: str) -> None:
        """Add a new MCP connector to mcp.json."""
        import json as _json

        try:
            from icarus.mcp.config import add_server

            server_def = _json.loads(config_json)
            await self._mutate_mcp_config("Added", add_server, name, server_def)
        except Exception as e:
            logger.exception("Failed to add connector %s", name)
            self._show_status(f"[red]Failed to add connector: {e}[/red]")

    async def _remove_connector(self, name: str) -> None:
        """Remove an MCP connector from mcp.json."""
        try:
            from icarus.mcp.config import remove_server

            await self._mutate_mcp_config("Removed", remove_server, name)
        except Exception as e:
            logger.exception("Failed to remove connector %s", name)
            self._show_status(f"[red]Failed to remove connector: {e}[/red]")

    async def _toggle_connector(self, name: str) -> None:
        """Toggle an MCP connector between enabled and disabled."""
        try:
            from icarus.mcp.config import toggle_server

            await self._mutate_mcp_config("Toggled", toggle_server, name)
        except Exception as e:
            logger.exception("Failed to toggle connector %s", name)
            self._show_status(f"[red]Failed to toggle connector: {e}[/red]")

    # ── Helpers ───────────────────────────────────────────────────────

    def _show_status(self, message: str) -> None:
        self.query_one("#skill-status", Static).update(message)

    def _reload(self) -> None:
        self._selected_index = 0
        self.run_worker(self._load_skills, exclusive=True)

    def action_cancel(self) -> None:
        self.dismiss(None)
