"""Tab / pane lifecycle for MainWindow.

Extracted verbatim from window.py as a mixin (matching WindowActions and the
other Window*Mixin modules) to shrink the window.py god-object. MainWindow
inherits this; methods keep their signatures and `self.` state access, so this
is a pure code move with no behavior change.

Covers tab close/rename/title, the tab context menus and their handlers
(_on_tabmenu_*), file-manager embed teardown (shared with shutdown), tab
attach/detach, drag-to-convert-to-split, and the layout toggle.

SplitViewTab is imported locally inside the methods that use it (a module-level
import here would be circular: split_view -> window -> window_tabs), so those
local imports are kept as-is.
"""

import logging
from typing import Optional

from gi.repository import Gtk, Adw, Gio, GLib, Gdk, GObject
from gettext import gettext as _

from sshpilot import icon_utils
from .connection_model import Connection
from .dnd_payload import decode_dnd_payload, new_internal_drop_target
from .terminal import TerminalWidget
from .plugins.api import Capability
from .plugins.registry import capabilities_for
from .connection_display import get_connection_alias, get_connection_host
from .file_manager_integration import launch_remote_file_manager
from .file_manager_integration import (
    has_internal_file_manager,
    should_hide_external_terminal_options,
    should_hide_file_manager_options,
)

logger = logging.getLogger(__name__)

# Match window.py's private aliases so the moved methods read unchanged.
_get_connection_host = get_connection_host
_get_connection_alias = get_connection_alias


def classify_tab_bar_hit(tab_bar, x, y):
    """Classify a click on *tab_bar* at (*x*, *y*).

    Adw.TabBar has no public hit-test API, so this walks ``pick()`` ancestors.
    Returns one of:
      ``('tab', page)`` — click on a private AdwTab (css_name ``tab``)
      ``('close',)``    — tab close button
      ``('action',)``   — start/end action chrome
      ``('empty',)``    — empty bar / tabbox / separator space
    """
    try:
        widget = tab_bar.pick(x, y, Gtk.PickFlags.DEFAULT)
    except Exception:
        return ('empty',)
    while widget is not None and widget is not tab_bar:
        try:
            if widget.has_css_class('tab-close-button'):
                return ('close',)
            if widget.has_css_class('start-action') or widget.has_css_class('end-action'):
                return ('action',)
        except Exception:
            pass
        try:
            if widget.get_css_name() == 'tab':
                return ('tab', widget.get_property('page'))
        except Exception:
            pass
        widget = widget.get_parent()
    return ('empty',)


def find_tab_page_at(tab_bar, x, y):
    """Return the Adw.TabPage under (*x*, *y*) on *tab_bar*, or None."""
    kind, *rest = classify_tab_bar_hit(tab_bar, x, y)
    return rest[0] if kind == 'tab' else None


class WindowTabsMixin:
    """Tab/pane lifecycle, context menus, teardown, and drag-to-split."""

    def _on_tab_close_confirmed(self, dialog, response_id, tab_view, page):
        """Handle response from tab close confirmation dialog"""
        dialog.destroy()
        if response_id == 'close':
            self._close_tab(tab_view, page)
        # If cancelled, do nothing - the tab remains open
    
    def _close_tab(self, tab_view, page):
        """Close the tab and clean up resources"""
        if hasattr(page, 'get_child'):
            child = page.get_child()
            if hasattr(child, 'disconnect'):
                # Get the connection associated with this terminal using reverse map
                connection = self.terminal_to_connection.get(child)
                # Disconnect the terminal
                child.disconnect()
                # Clean up multi-tab tracking maps
                try:
                    if connection is not None:
                        # Remove from list for this connection
                        if connection in self.connection_to_terminals and child in self.connection_to_terminals[connection]:
                            self.connection_to_terminals[connection].remove(child)
                            if not self.connection_to_terminals[connection]:
                                del self.connection_to_terminals[connection]
                        # Update most-recent mapping
                        if connection in self.active_terminals and self.active_terminals[connection] is child:
                            remaining = self.connection_to_terminals.get(connection)
                            if remaining:
                                self.active_terminals[connection] = remaining[-1]
                            else:
                                del self.active_terminals[connection]
                    if child in self.terminal_to_connection:
                        del self.terminal_to_connection[child]
                except Exception:
                    pass
        
        # Close the tab page
        tab_view.close_page(page)
        
        # Update the UI based on the number of remaining tabs
        GLib.idle_add(self._update_ui_after_tab_close)
    
    def _on_tab_bar_pressed(self, gesture, n_press, x, y):
        if n_press != 2:
            return
        kind, *rest = classify_tab_bar_hit(self.tab_bar, x, y)
        if kind == 'tab':
            page = rest[0]
            if page and not self._is_start_tab_page(page):
                self._show_tab_rename_popover(page, x, y)
            return
        if kind == 'empty':
            try:
                self.terminal_manager.show_local_terminal()
            except Exception as exc:
                logger.error("Tab-bar new local terminal failed: %s", exc)

    def _show_tab_rename_popover(self, page, x, y):
        entry = Gtk.Entry()
        entry.set_text(page.get_title())
        entry.set_width_chars(24)

        popover = Gtk.Popover()
        popover.set_child(entry)
        popover.set_parent(self.tab_bar)
        popover.set_has_arrow(False)

        rect = Gdk.Rectangle()
        rect.x, rect.y, rect.width, rect.height = int(x), int(y), 1, 1
        popover.set_pointing_to(rect)
        popover._committed = False

        def commit(_entry):
            if not popover._committed:
                popover._committed = True
                self._apply_tab_title(page, entry.get_text().strip())
            popover.popdown()

        def on_closed(p):
            if not p._committed:
                self._apply_tab_title(page, entry.get_text().strip())

        entry.connect('activate', commit)
        popover.connect('closed', on_closed)
        popover.popup()
        GLib.idle_add(lambda: (entry.grab_focus(), entry.select_region(0, -1), False)[-1])

    def _apply_tab_title(self, page, title):
        if title:
            page.set_title(title)
            page.custom_tab_title = title
        else:
            page.custom_tab_title = None
            terminal = page.get_child()
            if hasattr(terminal, 'connection'):
                page.set_title(terminal.connection.nickname)

    # ── tab context menu (right-click) ─────────────────────────────────────────

    # All tab-menu action names, in display order within their sections.
    _TAB_MENU_ACTIONS = (
        'tabmenu-duplicate', 'tabmenu-rename',
        'tabmenu-reconnect', 'tabmenu-manage-files',
        'tabmenu-show-files-panel', 'tabmenu-hide-files-panel',
        'tabmenu-show-terminal-panel', 'tabmenu-hide-terminal-panel',
        'tabmenu-open-system-terminal', 'tabmenu-new-local',
        'tabmenu-fm-new-window',
        'tabmenu-layout-horizontal', 'tabmenu-layout-vertical',
        'tabmenu-layout-default', 'tabmenu-layout-compact',
        'tabmenu-close', 'tabmenu-close-others', 'tabmenu-close-right',
    )

    def _build_tab_context_menus(self) -> None:
        """Build the single tab context menu model and register its actions.

        AdwTabBox builds the popover once from this model and caches it, so the
        same model serves every tab type. Per-tab differences are produced by
        enabling only the relevant actions in _on_tab_setup_menu; every item
        carries hidden-when="action-disabled" so disabled items vanish rather
        than grey out. Each win.<name> action operates on the right-clicked
        page captured in setup-menu (self._tab_menu_page).
        """
        handlers = {
            'tabmenu-duplicate': self._on_tabmenu_duplicate,
            'tabmenu-rename': self._on_tabmenu_rename,
            'tabmenu-reconnect': self._on_tabmenu_reconnect,
            'tabmenu-manage-files': self._on_tabmenu_manage_files,
            'tabmenu-show-files-panel': self._on_tabmenu_show_files_panel,
            'tabmenu-hide-files-panel': self._on_tabmenu_hide_files_panel,
            'tabmenu-show-terminal-panel': self._on_tabmenu_show_terminal_panel,
            'tabmenu-hide-terminal-panel': self._on_tabmenu_hide_terminal_panel,
            'tabmenu-open-system-terminal': self._on_tabmenu_open_system_terminal,
            'tabmenu-new-local': self._on_tabmenu_new_local,
            'tabmenu-fm-new-window': self._on_tabmenu_fm_new_window,
            'tabmenu-layout-horizontal': self._on_tabmenu_layout_horizontal,
            'tabmenu-layout-vertical': self._on_tabmenu_layout_vertical,
            'tabmenu-layout-default': self._on_tabmenu_layout_default,
            'tabmenu-layout-compact': self._on_tabmenu_layout_compact,
            'tabmenu-close': self._on_tabmenu_close,
            'tabmenu-close-others': self._on_tabmenu_close_others,
            'tabmenu-close-right': self._on_tabmenu_close_right,
        }
        for name, handler in handlers.items():
            action = Gio.SimpleAction.new(name, None)
            action.connect('activate', handler)
            self.add_action(action)

        def _item(label, action_name):
            item = Gio.MenuItem.new(label, 'win.' + action_name)
            # Hide the item entirely when its action is disabled, so each tab
            # type shows only its applicable items.
            item.set_attribute_value(
                'hidden-when', GLib.Variant('s', 'action-disabled')
            )
            return item

        menu = Gio.Menu()

        sec1 = Gio.Menu()
        sec1.append_item(_item(_('Duplicate'), 'tabmenu-duplicate'))
        sec1.append_item(_item(_('Rename Tab…'), 'tabmenu-rename'))
        menu.append_section(None, sec1)

        sec2 = Gio.Menu()
        sec2.append_item(_item(_('Reconnect'), 'tabmenu-reconnect'))
        sec2.append_item(_item(_('Manage Files'), 'tabmenu-manage-files'))
        sec2.append_item(_item(_('Show File Pane'), 'tabmenu-show-files-panel'))
        sec2.append_item(_item(_('Hide File Pane'), 'tabmenu-hide-files-panel'))
        sec2.append_item(_item(_('Show Terminal Pane'), 'tabmenu-show-terminal-panel'))
        sec2.append_item(_item(_('Hide Terminal Pane'), 'tabmenu-hide-terminal-panel'))
        sec2.append_item(_item(_('Open in System Terminal'), 'tabmenu-open-system-terminal'))
        sec2.append_item(_item(_('New Local Tab'), 'tabmenu-new-local'))
        sec2.append_item(_item(_('Open in New Window'), 'tabmenu-fm-new-window'))
        menu.append_section(None, sec2)

        sec3 = Gio.Menu()
        sec3.append_item(_item(_('Side by Side'), 'tabmenu-layout-horizontal'))
        sec3.append_item(_item(_('Top / Bottom'), 'tabmenu-layout-vertical'))
        sec3.append_item(_item(_('Default layout'), 'tabmenu-layout-default'))
        sec3.append_item(_item(_('Compact layout'), 'tabmenu-layout-compact'))
        menu.append_section(None, sec3)

        sec4 = Gio.Menu()
        sec4.append_item(_item(_('Close'), 'tabmenu-close'))
        sec4.append_item(_item(_('Close Other Tabs'), 'tabmenu-close-others'))
        sec4.append_item(_item(_('Close Tabs to the Right'), 'tabmenu-close-right'))
        menu.append_section(None, sec4)

        self._tab_menu_model = menu

    def _on_tab_bar_secondary_press(self, gesture, n_press, x, y):
        """Expose or hide the tab menu model before AdwTabBox builds the menu.

        Runs in the capture phase, ahead of AdwTabBox's own right-click handler.
        The pinned Start tab gets a NULL model (no menu); every other tab gets
        the full model. Pinned AdwTab widgets carry the "pinned" CSS class.
        """
        # Remember where the menu was opened so "Rename Tab…" can anchor its
        # popover at the right-clicked tab (tab_bar coordinate space).
        self._tab_menu_xy = (x, y)
        try:
            widget = self.tab_bar.pick(x, y, Gtk.PickFlags.DEFAULT)
            on_pinned = False
            while widget is not None and widget is not self.tab_bar:
                if widget.has_css_class('pinned'):
                    on_pinned = True
                    break
                widget = widget.get_parent()
            self.tab_view.set_menu_model(
                None if on_pinned else self._tab_menu_model
            )
        except Exception:
            logger.debug("Tab menu guard failed", exc_info=True)
            self.tab_view.set_menu_model(self._tab_menu_model)

    def _on_tab_setup_menu(self, tab_view, page):
        """Enable the actions applicable to the right-clicked tab type.

        page is None when the menu closes; keep _tab_menu_page so the
        just-activated action still targets the right tab.
        """
        if page is None:
            return
        self._tab_menu_page = page

        if self._is_start_tab_page(page):
            enabled = set()  # belt-and-braces; the capture guard already hid it
        else:
            enabled = self._enabled_tab_actions(page.get_child())

        for name in self._TAB_MENU_ACTIONS:
            action = self.lookup_action(name)
            if action is not None:
                action.set_enabled(name in enabled)

    def _file_manager_embed_for_child(self, child):
        """Return the first FileManagerTabEmbed in a tab's child subtree, or None.

        File-manager tabs wrap the embed inside a placeholder GtkBox, so a
        direct isinstance() check on the page child is insufficient.
        """
        embeds = self._file_manager_embeds_for_child(child)
        return embeds[0] if embeds else None

    def _file_manager_embeds_for_child(self, child):
        """Return ALL FileManagerTabEmbeds in a tab's child subtree.

        A split-view tab can hold several terminals, each with its own files
        panel, so teardown paths must not stop at the first embed found.
        """
        from .file_manager_integration import FileManagerTabEmbed
        found = []
        if child is None:
            return found
        if isinstance(child, FileManagerTabEmbed):
            found.append(child)
        w = child.get_first_child() if hasattr(child, 'get_first_child') else None
        while w is not None:
            found.extend(self._file_manager_embeds_for_child(w))
            w = w.get_next_sibling()
        return found

    def _teardown_file_manager_embed(self, embed) -> None:
        """Destroy an embedded file manager synchronously, outside GC.

        The embed carries Python 'destroy' signal handlers (its own _on_destroy
        plus the tracking lambda from _track_internal_file_manager_window) that
        form reference cycles. If the embed is finalized by the garbage
        collector, PyGObject invokes those Python handlers mid-collection, which
        segfaults. Disconnecting the handlers and disposing the controller here
        (at tab close / shutdown, outside GC) avoids that.
        """
        if embed is None:
            return
        # Idempotency: once torn down (_controller is None) there is nothing to
        # disconnect or dispose, so a second call (e.g. both on_tab_detached and
        # _teardown_all_file_manager_tabs firing) is a safe no-op.
        if getattr(embed, '_controller', None) is None:
            return
        try:
            destroy_id = GObject.signal_lookup('destroy', Gtk.Widget.__gtype__)
            GObject.signal_handlers_disconnect_matched(
                embed, GObject.SignalMatchType.ID, destroy_id, 0, None, None, None)
        except Exception:
            logger.debug('Could not disconnect FM embed destroy handlers', exc_info=True)
        self._teardown_embed_controller(embed, self._internal_file_manager_windows)

    @staticmethod
    def _teardown_embed_controller(embed, registry) -> bool:
        """Dispose an embed's file-manager controller. Pure/duck-typed: no GTK.

        Order matters and is asserted by tests: remove the controller from
        ``registry`` first, then run ``_cleanup_manager()``, then ``destroy()``,
        then null ``embed._controller`` so the embed reads as torn down.
        Idempotent — returns False (a no-op) when there is no live controller.
        """
        if embed is None or getattr(embed, '_controller', None) is None:
            return False
        controller = embed._controller
        try:
            if controller in registry:
                registry.remove(controller)
        except Exception:
            pass
        try:
            if hasattr(controller, '_cleanup_manager'):
                controller._cleanup_manager()
        except Exception:
            logger.debug('FM controller cleanup failed', exc_info=True)
        try:
            controller.destroy()
        except Exception:
            logger.debug('FM controller destroy failed', exc_info=True)
        try:
            embed._controller = None
        except Exception:
            pass
        return True

    def _teardown_all_file_manager_tabs(self) -> None:
        """Tear down every open file-manager tab synchronously (app shutdown)."""
        try:
            pages = [self.tab_view.get_nth_page(i)
                     for i in range(self.tab_view.get_n_pages())]
        except Exception:
            return
        for page in pages:
            try:
                embeds = self._file_manager_embeds_for_child(page.get_child())
            except Exception:
                embeds = []
            for embed in embeds:
                clear_panel = getattr(embed, 'clear_terminal_panel', None)
                if callable(clear_panel):
                    clear_panel()
                self._teardown_file_manager_embed(embed)
        # Lifecycle assertion: a tab whose embed still holds a live controller
        # after this pass is a leak that would later be finalized during GC and
        # segfault. Surface it in the log instead of failing silently.
        try:
            for page in pages:
                for embed in self._file_manager_embeds_for_child(page.get_child()):
                    if getattr(embed, '_controller', None) is None:
                        continue
                    logger.warning(
                        'File-manager embed survived shutdown teardown with a live '
                        'controller (%r) — potential GC-finalization crash source', embed)
        except Exception:
            logger.debug('FM teardown lifecycle check failed', exc_info=True)

    def _enabled_tab_actions(self, child) -> set:
        """Return the set of tab-menu action names to enable for child.

        Applies the same capability/preference gating as the sidebar menu for
        Manage Files, Open in System Terminal and (file manager) New Window.
        """
        from .split_view import SplitViewTab

        common = {'tabmenu-rename', 'tabmenu-close',
                  'tabmenu-close-others', 'tabmenu-close-right'}

        if isinstance(child, SplitViewTab):
            return common | {
                'tabmenu-layout-horizontal', 'tabmenu-layout-vertical',
                'tabmenu-layout-default', 'tabmenu-layout-compact',
            }

        # Terminal check must precede the embed search: a terminal with a
        # files panel (set_file_panel) contains a FileManagerTabEmbed in its
        # subtree but is still a terminal tab.
        if isinstance(child, TerminalWidget):
            try:
                is_local = child._is_local_terminal()
            except Exception:
                is_local = False
            enabled = common | {'tabmenu-duplicate'}
            if is_local:
                enabled.add('tabmenu-new-local')
                return enabled
            enabled.add('tabmenu-reconnect')
            conn = self.terminal_to_connection.get(child)
            caps = capabilities_for(conn) if conn else frozenset()
            if Capability.FILE_TRANSFER in caps and not should_hide_file_manager_options():
                enabled.add('tabmenu-manage-files')
                if conn is not None and has_internal_file_manager():
                    enabled.add(
                        'tabmenu-hide-files-panel' if child.has_file_panel()
                        else 'tabmenu-show-files-panel'
                    )
            if getattr(conn, 'protocol', 'ssh') == 'ssh' and not should_hide_external_terminal_options():
                enabled.add('tabmenu-open-system-terminal')
            return enabled

        embed = self._file_manager_embed_for_child(child)
        if embed is not None:
            enabled = set(common)
            if not should_hide_file_manager_options():
                enabled.add('tabmenu-fm-new-window')
            controller = getattr(embed, '_controller', None)
            conn = getattr(controller, '_connection', None) if controller else None
            if conn is not None and hasattr(embed, 'has_terminal_panel'):
                enabled.add(
                    'tabmenu-hide-terminal-panel' if embed.has_terminal_panel()
                    else 'tabmenu-show-terminal-panel'
                )
            return enabled

        # Plugin pages (Docker Console, …), WebTab, and other non-terminal
        # content: still allow rename / close / close-others / close-right.
        # Returning empty used to hide the entire menu (items use
        # hidden-when=action-disabled).
        return common

    # ── tab context menu action handlers ───────────────────────────────────────

    def _tab_menu_target(self):
        """Return (page, child) for the current tab menu target, or (None, None)."""
        page = getattr(self, '_tab_menu_page', None)
        if page is None:
            return None, None
        try:
            child = page.get_child()
        except Exception:
            child = None
        return page, child

    def _on_tabmenu_duplicate(self, action, param=None):
        try:
            page, child = self._tab_menu_target()
            if not isinstance(child, TerminalWidget):
                return
            if child._is_local_terminal():
                self.terminal_manager.show_local_terminal()
                return
            conn = self.terminal_to_connection.get(child)
            if conn is not None:
                self.terminal_manager.connect_to_host(conn, force_new=True)
        except Exception as exc:
            logger.error("Tab duplicate failed: %s", exc)

    def _on_tabmenu_rename(self, action, param=None):
        try:
            page, _child = self._tab_menu_target()
            if page is not None:
                self._rename_tab_page(page)
        except Exception as exc:
            logger.error("Tab rename failed: %s", exc)

    def _rename_tab_page(self, page) -> None:
        """Select the page and open the inline rename popover anchored to the tab bar."""
        try:
            self.tab_view.set_selected_page(page)
        except Exception:
            pass
        # Anchor at the right-clicked position (captured when the menu opened);
        # fall back near the start of the tab bar for keyboard-triggered menus.
        xy = getattr(self, '_tab_menu_xy', None)
        if xy is not None:
            x, y = xy
        else:
            x = max(0, min(40, self.tab_bar.get_width() // 2))
            y = self.tab_bar.get_height()
        self._show_tab_rename_popover(page, x, y)

    def _on_tabmenu_reconnect(self, action, param=None):
        try:
            page, child = self._tab_menu_target()
            if isinstance(child, TerminalWidget) and hasattr(child, '_on_reconnect_clicked'):
                child._on_reconnect_clicked()
        except Exception as exc:
            logger.error("Tab reconnect failed: %s", exc)

    def _on_tabmenu_manage_files(self, action, param=None):
        try:
            page, child = self._tab_menu_target()
            if not isinstance(child, TerminalWidget):
                return
            conn = self.terminal_to_connection.get(child)
            if conn is not None:
                self._open_manage_files_for_connection(conn)
        except Exception as exc:
            logger.error("Tab manage files failed: %s", exc)

    def _on_tabmenu_show_files_panel(self, action, param=None):
        """Embed the SFTP file manager below the tab's terminal (same session)."""
        try:
            page, child = self._tab_menu_target()
            if not isinstance(child, TerminalWidget) or child.has_file_panel():
                return
            conn = self.terminal_to_connection.get(child)
            if conn is None:
                return
            from .file_manager_integration import create_internal_file_manager_tab
            ssh_config = None
            try:
                ssh_config = self.config.get_ssh_config()
            except Exception:
                ssh_config = None
            host_value = _get_connection_host(conn) or _get_connection_alias(conn)
            port_value = getattr(conn, 'port', 22)
            widget, controller = create_internal_file_manager_tab(
                user=str(getattr(conn, 'username', '') or ''),
                host=str(host_value or ''),
                port=port_value if port_value and port_value != 22 else None,
                nickname=str(getattr(conn, 'nickname', '') or host_value or ''),
                parent_window=self,
                connection=conn,
                connection_manager=self.connection_manager,
                ssh_config=ssh_config,
            )
            self._track_internal_file_manager_window(controller, widget=widget)
            child.set_file_panel(
                widget,
                teardown=lambda: self._teardown_file_manager_embed(widget),
            )
        except Exception as exc:
            logger.error("Show files below terminal failed: %s", exc)

    def _on_tabmenu_hide_files_panel(self, action, param=None):
        try:
            page, child = self._tab_menu_target()
            if isinstance(child, TerminalWidget):
                child.clear_file_panel()
        except Exception as exc:
            logger.error("Hide files below terminal failed: %s", exc)

    def _on_tabmenu_show_terminal_panel(self, action, param=None):
        """Embed a terminal below the tab's file manager (same connection)."""
        try:
            page, child = self._tab_menu_target()
            embed = self._file_manager_embed_for_child(child)
            if embed is None or embed.has_terminal_panel():
                return
            controller = getattr(embed, '_controller', None)
            conn = getattr(controller, '_connection', None) if controller else None
            if conn is None:
                return
            terminal = self.terminal_manager.create_terminal_for_pane(conn)
            embed.set_terminal_panel(
                terminal,
                teardown=lambda: self.cleanup_embedded_terminal(terminal),
            )
        except Exception as exc:
            logger.error("Show terminal below file manager failed: %s", exc)

    def _on_tabmenu_hide_terminal_panel(self, action, param=None):
        try:
            page, child = self._tab_menu_target()
            embed = self._file_manager_embed_for_child(child)
            if embed is not None:
                embed.clear_terminal_panel()
        except Exception as exc:
            logger.error("Hide terminal below file manager failed: %s", exc)

    def cleanup_embedded_terminal(self, terminal) -> None:
        """Disconnect a terminal hosted outside a main tab page and drop it
        from the tracking dicts.

        Shared by split-view pane cleanup and the file-manager terminal panel —
        both host terminals whose lifecycle bypasses the main tab_view detach
        path.
        """
        try:
            if hasattr(terminal, 'clear_file_panel'):
                terminal.clear_file_panel()
        except Exception:
            logger.debug("Failed to clear files panel during terminal cleanup",
                         exc_info=True)
        try:
            terminal.disconnect()
        except Exception:
            pass
        try:
            connection = self.terminal_to_connection.get(terminal)
            if connection:
                terms = self.connection_to_terminals.get(connection, [])
                if terminal in terms:
                    terms.remove(terminal)
                    if not terms:
                        del self.connection_to_terminals[connection]
                if self.active_terminals.get(connection) is terminal:
                    remaining = self.connection_to_terminals.get(connection)
                    if remaining:
                        self.active_terminals[connection] = remaining[-1]
                    else:
                        self.active_terminals.pop(connection, None)
            self.terminal_to_connection.pop(terminal, None)
        except Exception as exc:
            logger.debug("Error cleaning up embedded terminal dicts: %s", exc)

    def _on_tabmenu_open_system_terminal(self, action, param=None):
        try:
            page, child = self._tab_menu_target()
            if not isinstance(child, TerminalWidget):
                return
            conn = self.terminal_to_connection.get(child)
            if conn is not None:
                self.open_in_system_terminal(conn)
        except Exception as exc:
            logger.error("Tab open in system terminal failed: %s", exc)

    def _on_tabmenu_new_local(self, action, param=None):
        try:
            self.terminal_manager.show_local_terminal()
        except Exception as exc:
            logger.error("Tab new local terminal failed: %s", exc)

    def _on_tabmenu_close(self, action, param=None):
        try:
            page, _child = self._tab_menu_target()
            if page is not None:
                self.tab_view.close_page(page)
        except Exception as exc:
            logger.error("Tab close failed: %s", exc)

    def _on_tabmenu_close_others(self, action, param=None):
        try:
            page, _child = self._tab_menu_target()
            if page is not None:
                self._confirm_then_bulk_close(
                    page,
                    lambda: self.tab_view.close_other_pages(page),
                    after_only=False,
                )
        except Exception as exc:
            logger.error("Tab close others failed: %s", exc)

    def _on_tabmenu_close_right(self, action, param=None):
        try:
            page, _child = self._tab_menu_target()
            if page is not None:
                self._confirm_then_bulk_close(
                    page,
                    lambda: self.tab_view.close_pages_after(page),
                    after_only=True,
                )
        except Exception as exc:
            logger.error("Tab close to the right failed: %s", exc)

    def _apply_split_layout_to_target(self, mode):
        page, child = self._tab_menu_target()
        from .split_view import SplitViewTab
        if isinstance(child, SplitViewTab):
            child.set_layout_mode(mode)

    def _on_tabmenu_layout_horizontal(self, action, param=None):
        try:
            self._apply_split_layout_to_target('horizontal')
        except Exception as exc:
            logger.error("Tab layout horizontal failed: %s", exc)

    def _on_tabmenu_layout_vertical(self, action, param=None):
        try:
            self._apply_split_layout_to_target('vertical')
        except Exception as exc:
            logger.error("Tab layout vertical failed: %s", exc)

    def _on_tabmenu_layout_default(self, action, param=None):
        try:
            page, child = self._tab_menu_target()
            from .split_view import SplitViewTab
            if isinstance(child, SplitViewTab):
                child.fill_panes_to_viewport()
        except Exception as exc:
            logger.error("Tab layout default failed: %s", exc)

    def _on_tabmenu_layout_compact(self, action, param=None):
        try:
            page, child = self._tab_menu_target()
            from .split_view import SplitViewTab
            if isinstance(child, SplitViewTab):
                child.reset_all_row_heights(0.3)
        except Exception as exc:
            logger.error("Tab layout compact failed: %s", exc)

    def _on_tabmenu_fm_new_window(self, action, param=None):
        try:
            page, child = self._tab_menu_target()
            embed = self._file_manager_embed_for_child(child)
            if embed is None:
                return
            controller = getattr(embed, '_controller', None)
            conn = getattr(controller, '_connection', None) if controller else None
            if conn is None:
                logger.debug("File manager tab has no connection for new window")
                return
            self._launch_external_file_manager(conn)
        except Exception as exc:
            logger.error("Tab file manager new window failed: %s", exc)

    def _launch_external_file_manager(self, connection) -> None:
        """Open a standalone (external) file manager window for connection.

        Same path used by the file_manager.open_externally preference.
        """
        nickname = (
            getattr(connection, 'nickname', None)
            or getattr(connection, 'hostname', None)
            or getattr(connection, 'host', None)
            or getattr(connection, 'username', 'Remote Host')
        )
        host_value = _get_connection_host(connection) or _get_connection_alias(connection)
        username = getattr(connection, 'username', '') or ''
        port_value = getattr(connection, 'port', 22)
        effective_port = port_value if port_value and port_value != 22 else None

        ssh_config = None
        if hasattr(self, 'config') and self.config is not None:
            try:
                ssh_config = self.config.get_ssh_config()
            except Exception as exc:
                logger.debug("Failed to read SSH configuration for file manager: %s", exc)
                ssh_config = None

        def error_callback(error_msg):
            message = error_msg or "Failed to open file manager"
            logger.error(f"Failed to open file manager for {nickname}: {message}")
            self._show_manage_files_error(str(nickname), message)

        success, error_msg, window = launch_remote_file_manager(
            user=str(username or ''),
            host=str(host_value or ''),
            port=effective_port,
            nickname=str(nickname),
            parent_window=self,
            error_callback=error_callback,
            connection=connection,
            connection_manager=self.connection_manager,
            ssh_config=ssh_config,
        )

        if success:
            logger.info(f"Started external file manager for {nickname}")
            if window is not None:
                self._track_internal_file_manager_window(window)
        else:
            message = error_msg or "Failed to start file manager process"
            logger.error(f"Failed to start file manager process for {nickname}: {message}")
            self._show_manage_files_error(str(nickname), message)

    def _bulk_close_target_pages(self, target_page, after_only: bool):
        """Pages that close_other_pages / close_pages_after would remove.

        get_pages() yields pages in tab order, so "after target" is every page
        seen once the target has been passed. The target itself and the pinned
        Start tab are excluded, matching libadwaita's semantics.
        """
        pages, seen_target = [], False
        for p in list(self.tab_view.get_pages()):
            if p is target_page:
                seen_target = True
                continue
            if self._is_start_tab_page(p):
                continue
            if after_only and not seen_target:
                continue
            pages.append(p)
        return pages

    def _count_sessions_in_pages(self, pages) -> int:
        """Number of live terminal sessions across the given pages."""
        from .split_view import SplitViewTab
        total = 0
        for p in pages:
            child = p.get_child() if hasattr(p, 'get_child') else None
            if isinstance(child, SplitViewTab):
                total += sum(pane.get_terminal_count() for pane in child._panes)
            elif child in self.terminal_to_connection:
                total += 1
        return total

    def _run_suppressed_close(self, close_fn):
        """Run a bulk close with the per-tab disconnect confirmation suppressed.

        The bulk close emits close-page once per page; suppressing keeps
        on_tab_close from spawning a modal dialog for each (issue #1014). The
        closes run synchronously, so the flag is safely reset afterwards.
        Sessions still tear down via TerminalWidget._on_destroy.
        """
        self._suppress_close_confirmation = True
        try:
            close_fn()
        finally:
            self._suppress_close_confirmation = False

    def _add_disconnect_opt_out(self, dialog):
        """Add the shared opt-out control to a disconnect confirmation."""
        checkbox = Gtk.CheckButton(label=_("Don't ask me again"))
        checkbox.set_halign(Gtk.Align.START)
        checkbox.set_margin_top(6)
        dialog.set_extra_child(checkbox)
        return checkbox

    def _persist_disconnect_opt_out(self, checkbox):
        """Disable future disconnect confirmations when explicitly requested."""
        if checkbox.get_active():
            self.config.set_setting('confirm-disconnect', False)

    def _on_bulk_close_response(self, dialog, response_id, close_fn, checkbox):
        """Handle the single confirmation dialog for a bulk tab close."""
        if response_id == 'close':
            self._persist_disconnect_opt_out(checkbox)
            self._run_suppressed_close(close_fn)
        dialog.destroy()

    def _confirm_then_bulk_close(self, target_page, close_fn, after_only: bool):
        """Close other / to-the-right tabs, honoring confirm-disconnect.

        When the preference is on and live sessions would be disconnected, ask
        once for the whole batch rather than once per tab. On confirm the batch
        closes with per-tab confirmation suppressed; on cancel nothing happens.
        """
        pages = self._bulk_close_target_pages(target_page, after_only)
        if not pages:
            return

        confirm = bool(
            getattr(self, 'config', None)
            and self.config.get_setting('confirm-disconnect', True)
        )
        n_sessions = self._count_sessions_in_pages(pages)

        if confirm and n_sessions > 0:
            dialog = Adw.MessageDialog(
                transient_for=self,
                modal=True,
                heading=_("Close tabs?"),
                body=_("This will close {t} tab(s) and disconnect {n} session(s). Continue?").format(
                    t=len(pages), n=n_sessions
                ),
            )
            dialog.add_response('cancel', _("Cancel"))
            dialog.add_response('close', _("Close"))
            dialog.set_response_appearance('close', Adw.ResponseAppearance.DESTRUCTIVE)
            dialog.set_default_response('close')
            dialog.set_close_response('cancel')
            checkbox = self._add_disconnect_opt_out(dialog)
            dialog.connect(
                'response',
                self._on_bulk_close_response,
                close_fn,
                checkbox,
            )
            dialog.present()
        else:
            # Toggle off, or nothing with a live session to disconnect: close
            # directly. With the toggle off, on_tab_close disconnects each tab
            # without a dialog.
            close_fn()

    def on_tab_close(self, tab_view, page):
        """Handle tab close - THE KEY FIX: Never call close_page ourselves"""
        if self._is_start_tab_page(page):
            return True

        # If we are closing pages programmatically (e.g., after deleting a
        # connection), suppress the confirmation dialog and allow the default
        # close behavior to proceed.
        if getattr(self, '_suppress_close_confirmation', False):
            return False

        # SplitViewTab: clean up all embedded terminals, then allow immediate close.
        # When confirm-disconnect is enabled and there are active terminals, ask first.
        if hasattr(page, 'get_child'):
            child = page.get_child()
            from .split_view import SplitViewTab
            if isinstance(child, SplitViewTab):
                n_terminals = sum(p.get_terminal_count() for p in child._panes)
                confirm_disconnect = getattr(self, 'config', None) and self.config.get_setting('confirm-disconnect', True)
                if confirm_disconnect and n_terminals > 0:
                    self._pending_close_split_tab_view = tab_view
                    self._pending_close_split_page = page
                    self._pending_close_split_child = child
                    dialog = Adw.MessageDialog(
                        transient_for=self,
                        modal=True,
                        heading=_("Close split view?"),
                        body=_("This will disconnect {n} terminal session(s). Continue?").format(n=n_terminals),
                    )
                    dialog.add_response('cancel', _("Cancel"))
                    dialog.add_response('close', _("Close"))
                    dialog.set_response_appearance('close', Adw.ResponseAppearance.DESTRUCTIVE)
                    dialog.set_default_response('close')
                    dialog.set_close_response('cancel')
                    checkbox = self._add_disconnect_opt_out(dialog)
                    dialog.connect(
                        'response',
                        self._on_split_tab_close_response,
                        checkbox,
                    )
                    dialog.present()
                    return True  # Prevent immediate close; dialog handles it
                child.cleanup_all()
                return False

        # Get the connection for this tab
        connection = None
        terminal = None
        if hasattr(page, 'get_child'):
            child = page.get_child()
            if hasattr(child, 'disconnect'):
                terminal = child
                connection = self.terminal_to_connection.get(child)
        
        if not connection:
            # Non-terminal tabs (plugins, file manager, …) close immediately.
            return False
        
        # Check if confirmation is required
        confirm_disconnect = self.config.get_setting('confirm-disconnect', True)
        
        if confirm_disconnect:
            # Store tab view and page as instance variables
            self._pending_close_tab_view = tab_view
            self._pending_close_page = page
            self._pending_close_connection = connection
            self._pending_close_terminal = terminal
            
            # Show confirmation dialog
            host_value = _get_connection_host(connection) or _get_connection_alias(connection)
            dialog = Adw.MessageDialog(
                transient_for=self,
                modal=True,
                heading=_("Close connection to {name}").format(name=connection.nickname or host_value),
                body=_("Are you sure you want to close this connection?")
            )
            dialog.add_response('cancel', _("Cancel"))
            dialog.add_response('close', _("Close"))
            dialog.set_response_appearance('close', Adw.ResponseAppearance.DESTRUCTIVE)
            dialog.set_default_response('close')
            dialog.set_close_response('cancel')
            checkbox = self._add_disconnect_opt_out(dialog)
            
            # Connect to response signal before showing the dialog
            dialog.connect('response', self._on_tab_close_response, checkbox)
            dialog.present()
            
            # Prevent the default close behavior while we show confirmation
            return True
        else:
            # Explicitly disconnect so the SSH process (including any port
            # forwarding) is killed immediately rather than relying on the
            # widget destroy signal, which can be deferred indefinitely.
            if terminal and hasattr(terminal, 'disconnect'):
                terminal.disconnect()
            return False

    def _on_tab_close_response(self, dialog, response_id, checkbox):
        """Handle the response from the close confirmation dialog."""
        # Retrieve the pending tab info
        tab_view = self._pending_close_tab_view
        page = self._pending_close_page
        terminal = self._pending_close_terminal

        if response_id == 'close':
            self._persist_disconnect_opt_out(checkbox)
            # User confirmed, disconnect the terminal. The tab will be removed
            # by the AdwTabView once we finish the close operation.
            if terminal and hasattr(terminal, 'disconnect'):
                terminal.disconnect()
            # Now, tell the tab view to finish closing the page.
            tab_view.close_page_finish(page, True)
            
            # Update tab button visibility after closing
            self._update_tab_button_visibility()
            
            # Check if this was the last user tab
            if not self.has_user_tabs():
                self.show_start_tab()
        else:
            # User cancelled, so we reject the close request.
            # This is the critical step that makes the close button work again.
            tab_view.close_page_finish(page, False)

        dialog.destroy()
        # Clear pending state to avoid memory leaks
        self._pending_close_tab_view = None
        self._pending_close_page = None
        self._pending_close_connection = None
        self._pending_close_terminal = None

    def _on_split_tab_close_response(self, dialog, response_id, checkbox):
        """Handle the confirmation dialog for closing an entire SplitViewTab."""
        tab_view = getattr(self, '_pending_close_split_tab_view', None)
        page = getattr(self, '_pending_close_split_page', None)
        child = getattr(self, '_pending_close_split_child', None)
        if response_id == 'close':
            self._persist_disconnect_opt_out(checkbox)
            if child is not None:
                child.cleanup_all()
            if tab_view is not None and page is not None:
                tab_view.close_page_finish(page, True)
            self._update_tab_button_visibility()
            if tab_view is not None and not self.has_user_tabs():
                self.show_start_tab()
        else:
            if tab_view is not None and page is not None:
                tab_view.close_page_finish(page, False)
        dialog.destroy()
        self._pending_close_split_tab_view = None
        self._pending_close_split_page = None
        self._pending_close_split_child = None

    def on_tab_attached(self, tab_view, page, position):
        """Handle tab attached"""
        self._update_tab_button_visibility()
        # Register a drop target on TerminalWidget pages so dragging a
        # connection onto a terminal converts that tab into a split-view tab.
        try:
            from .terminal import TerminalWidget
            child = page.get_child() if page else None
            if isinstance(child, TerminalWidget):
                self._register_convert_to_split_drop(child, page)
        except Exception as exc:
            logger.debug("Could not register convert-to-split drop: %s", exc)
        self._update_layout_toggle_state()

        # Sidebar behavior: a session (SSH or local) just opened. Hide after a
        # short delay so the terminal settles, then the sidebar slides away.
        # The pinned Start page attaches at startup too — it is not a session,
        # so it must not trigger hide/minimize. Detect it by child type: at
        # attach time `_start_tab_page` isn't assigned yet (prepend() emits
        # page-attached synchronously before the return value is stored), so an
        # identity check via _is_start_tab_page() would miss it.
        try:
            from .welcome_page import WelcomePage
            start_child = page.get_child() if page else None
            is_start = self._is_start_tab_page(page) or isinstance(start_child, WelcomePage)
        except Exception:
            is_start = self._is_start_tab_page(page)
        try:
            action = self._sidebar_on_terminal_open() if not is_start else 'none'
            if action == 'hide':
                self._cancel_pending_sidebar_hide()
                self._sidebar_hide_timer_id = GLib.timeout_add(
                    350, self._hide_sidebar_after_terminal
                )
            elif action == 'minimize':
                self._cancel_pending_sidebar_hide()
                self._sidebar_hide_timer_id = GLib.timeout_add(
                    350, self._minimize_sidebar_after_terminal
                )
        except Exception:
            logger.debug("sidebar-on-terminal-open failed", exc_info=True)

    def _register_convert_to_split_drop(self, terminal, page) -> None:
        """Attach a drop target to terminal so dragging a connection converts the tab to split view."""
        dt = new_internal_drop_target()

        def _on_drop(_target, value, _x, _y):
            try:
                value = decode_dnd_payload(value)
                if not isinstance(value, dict):
                    return False

                drag_type = value.get("type")

                if drag_type == "command_block":
                    cmd_dict = {
                        'id': value.get('command_id'),
                        'name': value.get('name', ''),
                        'command': value.get('command', ''),
                        'has_placeholders': value.get('has_placeholders', False),
                    }
                    panel = getattr(self, 'command_blocks_panel', None)
                    if panel is not None:
                        panel._send_command_to_terminal(cmd_dict)
                    else:
                        data = (cmd_dict['command'] + '\n').encode('utf-8')
                        try:
                            terminal.feed_child_data(data)
                        except Exception:
                            pass
                    return True

                if drag_type not in ("connection", "group"):
                    return False

                # Only convert if the terminal is still in the main tab_view
                source_page = self._page_for_child(terminal)
                if source_page is None:
                    return False

                tab_title = _("Split View")

                if drag_type == "connection":
                    nicknames = value.get("connection_nicknames") or []
                    if not nicknames and value.get("connection_nickname"):
                        nicknames = [value["connection_nickname"]]
                    if not nicknames:
                        return False
                    connections = []
                    for nick in nicknames:
                        conn = self.connection_manager.find_connection_by_nickname(nick)
                        if conn is not None:
                            connections.append(conn)
                else:  # group
                    group_id = value.get("group_id")
                    group_info = self.group_manager.groups.get(group_id) if group_id else None
                    if not group_info:
                        return False
                    group_name = group_info.get("name", "")
                    if group_name:
                        tab_title = _("Split View — {name}").format(name=group_name)
                    connections = []
                    for nick in group_info.get("connections", []):
                        conn = self.connection_manager.find_connection_by_nickname(nick)
                        if conn is not None:
                            connections.append(conn)

                if not connections:
                    return False

                from .split_view import SplitViewTab

                svt = SplitViewTab(self)

                # Move the existing terminal out of the main tab_view into pane 0
                title = source_page.get_title() or _("Terminal")
                self._suppress_close_confirmation = True
                self._moving_tab_to_pane = True
                try:
                    self.tab_view.close_page(source_page)
                finally:
                    self._suppress_close_confirmation = False
                    self._moving_tab_to_pane = False
                # Defer reparent so tab_view fully completes its close sequence.
                # Pane 1 is filled synchronously below, so the proportional
                # paned settles before pane 0 gets its (deferred) terminal — that
                # leaves the divider at the empty placeholder's natural size, not
                # 50/50. Re-equalize once the terminal is embedded so the panes
                # match every other split scenario.
                def _embed_terminal_in_pane0():
                    svt._panes[0].add_terminal(terminal, title)
                    # _on_default_clicked resets horizontal splits to 50/50 and
                    # applies the default sizing for the pane count (fill for ≤2,
                    # 50% rows for more) — same as a freshly-opened split.
                    GLib.idle_add(lambda: (svt._on_default_clicked(), False)[1])
                    return False
                GLib.idle_add(_embed_terminal_in_pane0)

                # Add each dropped connection to pane 1 (and extra panes beyond)
                for i, conn in enumerate(connections):
                    if i == 0:
                        svt._panes[1].add_connection(conn)
                    else:
                        svt.add_pane().add_connection(conn)

                # Append the split-view tab to the main tab_view
                new_page = self.tab_view.append(svt)
                new_page.set_title(tab_title)
                try:
                    new_page.set_icon(
                        icon_utils.new_gicon_from_icon_name('view-dual-symbolic')
                    )
                except Exception:
                    pass
                svt._tab_page = new_page
                self.show_tab_view()
                self.tab_view.set_selected_page(new_page)
                return True
            except Exception as exc:
                logger.error("Convert-to-split drop failed: %s", exc)
                return False

        dt.connect("drop", _on_drop)
        dt.connect("enter", lambda _t, _x, _y: Gdk.DragAction.MOVE)
        terminal.add_controller(dt)
        # Remember it so SplitPane.add_terminal can detach it when this terminal
        # is embedded into a pane (otherwise it keeps intercepting connection
        # drops over the terminal area, so only the tab bar would accept them).
        terminal._convert_to_split_dt = dt

    # ── layout toggle state / apply ───────────────────────────────────────────

    def _update_layout_toggle_state(self) -> None:
        """Sync tab-bar H/V toggles with the selected tab."""
        if not hasattr(self, '_layout_h_btn'):
            return
        try:
            page = self.tab_view.get_selected_page()
            child = page.get_child() if page else None
            from .split_view import SplitViewTab
            is_terminal_tab = isinstance(child, (TerminalWidget, SplitViewTab))
            self._layout_h_btn.set_visible(is_terminal_tab)
            self._layout_v_btn.set_visible(is_terminal_tab)
            self._layout_toggle_updating[0] = True
            try:
                if isinstance(child, SplitViewTab):
                    mode = child.get_layout_mode()
                    self._layout_h_btn.set_active(mode == 'horizontal')
                    self._layout_v_btn.set_active(mode == 'vertical')
                else:
                    self._layout_h_btn.set_active(False)
                    self._layout_v_btn.set_active(False)
            finally:
                self._layout_toggle_updating[0] = False
        except Exception as exc:
            logger.debug("Failed to update layout toggle state: %s", exc)

    def _apply_tab_layout_mode(self, mode: str) -> None:
        """Apply H or V layout to the current tab (converts regular tab if needed)."""
        try:
            page = self.tab_view.get_selected_page()
            if page is None:
                return
            child = page.get_child()
            from .split_view import SplitViewTab
            if isinstance(child, SplitViewTab):
                child.set_layout_mode(mode)
            elif isinstance(child, TerminalWidget):
                self._convert_terminal_tab_to_split(page, child, mode)
        except Exception as exc:
            logger.error("Failed to apply tab layout mode: %s", exc)

    def _convert_terminal_tab_to_split(self, source_page, terminal, mode: str) -> None:
        """Convert a regular terminal tab into a split-view tab."""
        from .split_view import SplitViewTab

        svt = SplitViewTab(self)
        svt.set_layout_mode(mode)

        title = source_page.get_title() or _("Terminal")
        self._suppress_close_confirmation = True
        self._moving_tab_to_pane = True
        try:
            self.tab_view.close_page(source_page)
        finally:
            self._suppress_close_confirmation = False
            self._moving_tab_to_pane = False

        # Defer reparent so tab_view fully completes its close sequence first
        GLib.idle_add(svt._panes[0].add_terminal, terminal, title)

        new_page = self.tab_view.append(svt)
        new_page.set_title(_("Split View"))
        try:
            new_page.set_icon(
                icon_utils.new_gicon_from_icon_name('view-dual-symbolic')
            )
        except Exception:
            pass
        svt._tab_page = new_page
        self.show_tab_view()
        self.tab_view.set_selected_page(new_page)

    # ── tab button visibility ─────────────────────────────────────────────────

    def _update_tab_button_visibility(self):
        """Hide tab bar and overview button when only the Start tab is open."""
        try:
            show_tabs = self.has_user_tabs()
            if hasattr(self, 'tab_button'):
                self.tab_button.set_visible(show_tabs)
            if hasattr(self, 'tab_bar'):
                self.tab_bar.set_visible(show_tabs)
        except Exception as e:
            logger.error(f"Failed to update tab button visibility: {e}")

    def on_tab_detached(self, tab_view, page, position):
        """Handle tab detached"""
        # Skip dict cleanup when a terminal is being moved into a split pane
        # (the terminal stays live; its tracking entries remain valid).
        if getattr(self, '_moving_tab_to_pane', False):
            self._update_tab_button_visibility()
            if tab_view.get_n_pages() <= 1:
                self.show_start_tab()
            return

        # Tear down an embedded file manager synchronously (outside GC) so its
        # Python 'destroy' handlers never run during a garbage collection,
        # which segfaults. Covers ×, Close, Close Other Tabs, Close to Right.
        try:
            embeds = (
                self._file_manager_embeds_for_child(page.get_child())
                if page is not None and hasattr(page, 'get_child')
                else []
            )
            for embed in embeds:
                # A live terminal panel below the manager must be disconnected
                # too (its lifecycle bypasses the terminal-tab close paths).
                clear_panel = getattr(embed, 'clear_terminal_panel', None)
                if callable(clear_panel):
                    clear_panel()
                self._teardown_file_manager_embed(embed)
        except Exception:
            logger.debug('File manager tab teardown on detach failed', exc_info=True)

        # Cleanup terminal-to-connection maps when a page is detached
        detached_connection = None
        try:
            if hasattr(page, 'get_child'):
                child = page.get_child()
                if child in self.terminal_to_connection:
                    connection = self.terminal_to_connection.get(child)
                    detached_connection = connection
                    # Remove reverse map
                    del self.terminal_to_connection[child]
                    # Remove from per-connection list
                    if connection in self.connection_to_terminals and child in self.connection_to_terminals[connection]:
                        self.connection_to_terminals[connection].remove(child)
                        if not self.connection_to_terminals[connection]:
                            del self.connection_to_terminals[connection]
                    # Update most recent mapping if needed
                    if connection in self.active_terminals and self.active_terminals[connection] is child:
                        remaining = self.connection_to_terminals.get(connection)
                        if remaining:
                            self.active_terminals[connection] = remaining[-1]
                        else:
                            del self.active_terminals[connection]
        except Exception:
            pass

        # Update tab button visibility
        self._update_tab_button_visibility()
        self._update_layout_toggle_state()

        # Select Start when the last user tab closes
        if not self.has_user_tabs():
            self.show_start_tab()

        # Recompute the affected connection's state now that the terminal has
        # been removed from the maps. With no terminals left this resolves to
        # UNKNOWN, hiding the sidebar status icon instead of leaving a stale red
        # "Disconnected" indicator after an intentional close.
        if detached_connection is not None:
            try:
                self._recompute_connection_state(detached_connection)
            except Exception:
                pass

    def on_open_split_view_clicked(self, button):
        """Open a new empty split-view tab."""
        try:
            from .split_view import SplitViewTab
            svt = SplitViewTab(self)
            page = self.tab_view.append(svt)
            page.set_title(_("Split View"))
            page.set_icon(icon_utils.new_gicon_from_icon_name('view-dual-symbolic'))
            svt._tab_page = page
            self.show_tab_view()
            self.tab_view.set_selected_page(page)
        except Exception as exc:
            logger.error("Failed to open split view: %s", exc)

    def on_local_terminal_button_clicked(self, button):
        """Handle local terminal button click"""
        try:
            logger.info("Local terminal button clicked")
            self.terminal_manager.show_local_terminal()
        except Exception as e:
            logger.error(f"Failed to open local terminal: {e}")

    def on_tab_button_clicked(self, button):
        """Toggle the tab overview."""
        try:
            is_open = self.tab_overview.get_open()
            self.tab_overview.set_open(not is_open)
        except Exception as e:
            logger.error(f"Failed to toggle tab overview: {e}")

    def _page_for_child(self, child):
        """Return the Adw.TabPage for ``child`` in the main tab_view, or None.

        Unlike ``tab_view.get_page(child)``, this never emits the
        ``child_belongs_to_this_view`` CRITICAL assertion when ``child`` isn't in
        the main view — e.g. a terminal that has been embedded in a split-view
        pane (which lives in that pane's own inner tab view, not tab_view).
        """
        try:
            pages = self.tab_view.get_pages()
            for i in range(pages.get_n_items()):
                page = pages.get_item(i)
                if page is not None and page.get_child() is child:
                    return page
        except Exception:
            pass
        return None

    def _select_tab_relative(self, delta: int):
        """Select tab relative to current index, wrapping around."""
        self._return_to_tab_view_if_welcome()
        try:
            n = self.tab_view.get_n_pages()
            if n <= 0:
                return
            current = self.tab_view.get_selected_page()
            # If no current selection, pick first
            if not current:
                page = self.tab_view.get_nth_page(0)
                if page:
                    self.tab_view.set_selected_page(page)
                return
            # Find current index
            idx = 0
            for i in range(n):
                if self.tab_view.get_nth_page(i) == current:
                    idx = i
                    break
            new_index = (idx + delta) % n
            page = self.tab_view.get_nth_page(new_index)
            if page:
                self.tab_view.set_selected_page(page)
        except Exception:
            pass

    def _move_tab_relative(self, delta: int):
        """Reorder the selected tab one position left (-1) or right (+1)."""
        try:
            page = self.tab_view.get_selected_page()
            if page is None:
                return
            if delta < 0:
                self.tab_view.reorder_backward(page)
            elif delta > 0:
                self.tab_view.reorder_forward(page)
        except Exception:
            pass

    def _close_active_tab_or_pane(self):
        """Close the current tab — but in a split-view tab, close the focused
        pane instead (Ctrl+Shift+W is context-dependent)."""
        try:
            page = self.tab_view.get_selected_page()
            if page is None or self._is_start_tab_page(page):
                return
            child = page.get_child()
            from .split_view import SplitViewTab
            if isinstance(child, SplitViewTab) and hasattr(child, 'close_focused_pane'):
                child.close_focused_pane()
                return
            self.tab_view.close_page(page)
        except Exception:
            pass

    def _focus_most_recent_tab(self, connection: Connection) -> None:
        """Focus the most recent tab for a connection if one exists.

        Does nothing if the connection has no open tabs.
        """
        try:
            terms_for_conn = []
            try:
                n = self.tab_view.get_n_pages()
            except Exception:
                n = 0
            for i in range(n):
                page = self.tab_view.get_nth_page(i)
                child = page.get_child() if hasattr(page, 'get_child') else None
                if child is not None and self.terminal_to_connection.get(child) == connection:
                    terms_for_conn.append(child)

            if not terms_for_conn:
                return

            target_term = self.active_terminals.get(connection)
            if target_term not in terms_for_conn:
                target_term = terms_for_conn[0]

            page = self._page_for_child(target_term)
            if page is None:
                return

            if self.tab_view.get_selected_page() != page:
                self.tab_view.set_selected_page(page)

            self.active_terminals[connection] = target_term
            self._focus_terminal_widget(target_term)
        except Exception as e:
            logger.error(f"Failed to focus most recent tab for {getattr(connection, 'nickname', '')}: {e}")


    def _focus_most_recent_tab_or_open_new(self, connection: Connection):
        """If there are open tabs for this server, focus the most recent one.
        Otherwise open a new tab for the server.
        """
        self._return_to_tab_view_if_welcome()
        # Executing a result dismisses search (and restores the icon strip if
        # search expanded it). No-op when not searching.
        self._close_search_if_open()
        try:
            # Check if there are open tabs for this connection
            terms_for_conn = []
            try:
                n = self.tab_view.get_n_pages()
            except Exception:
                n = 0
            for i in range(n):
                page = self.tab_view.get_nth_page(i)
                child = page.get_child() if hasattr(page, 'get_child') else None
                if child is not None and self.terminal_to_connection.get(child) == connection:
                    terms_for_conn.append(child)

            if terms_for_conn:
                # Focus the most recent tab for this connection
                most_recent_term = self.active_terminals.get(connection)
                if most_recent_term and most_recent_term in terms_for_conn:
                    # Use the most recent terminal
                    target_term = most_recent_term
                else:
                    # Fallback to the first tab for this connection
                    target_term = terms_for_conn[0]
                
                page = self._page_for_child(target_term)
                if page is not None:
                    self.tab_view.set_selected_page(page)
                    # Update most-recent mapping
                    self.active_terminals[connection] = target_term
                    # Give focus to the VTE terminal so user can start typing immediately
                    self._focus_terminal_widget(target_term)
                    return

            # No existing tabs for this connection -> open a new one
            self.terminal_manager.connect_to_host(connection, force_new=False)
        except Exception as e:
            logger.error(f"Failed to focus most recent tab or open new for {getattr(connection, 'nickname', '')}: {e}")

    def _cycle_connection_tabs_or_open(self, connection: Connection):
        """If there are open tabs for this server, cycle to the next one (wrap).
        Otherwise open a new tab for the server.
        """
        self._return_to_tab_view_if_welcome()
        # Executing a result dismisses search (and restores the icon strip if
        # search expanded it). No-op when not searching.
        self._close_search_if_open()
        try:
            # Collect current pages in visual/tab order
            terms_for_conn = []
            try:
                n = self.tab_view.get_n_pages()
            except Exception:
                n = 0
            for i in range(n):
                page = self.tab_view.get_nth_page(i)
                child = page.get_child() if hasattr(page, 'get_child') else None
                if child is not None and self.terminal_to_connection.get(child) == connection:
                    terms_for_conn.append(child)

            if terms_for_conn:
                # Determine current index among this connection's tabs
                selected = self.tab_view.get_selected_page()
                current_idx = -1
                if selected is not None:
                    current_child = selected.get_child()
                    for i, t in enumerate(terms_for_conn):
                        if t == current_child:
                            current_idx = i
                            break
                # Compute next index (wrap)
                next_idx = (current_idx + 1) % len(terms_for_conn) if current_idx >= 0 else 0
                next_term = terms_for_conn[next_idx]
                page = self._page_for_child(next_term)
                if page is not None:
                    self.tab_view.set_selected_page(page)
                    # Update most-recent mapping
                    self.active_terminals[connection] = next_term
                    self._focus_terminal_widget(next_term)
                    return

            # No existing tabs for this connection -> open a new one
            self.terminal_manager.connect_to_host(connection, force_new=False)
            try:
                self._focus_most_recent_tab(connection)
            except Exception:
                pass
        except Exception as e:
            logger.error(f"Failed to cycle or open for {getattr(connection, 'nickname', '')}: {e}")

    def _focus_terminal_widget(self, terminal: TerminalWidget) -> None:
        """Request focus for a terminal widget, retrying on idle if needed."""

        if terminal is None:
            return

        def _omni_is_visible() -> bool:
            omni = getattr(self, '_omni_search', None)
            return bool(omni is not None and omni.popup.visible)

        def _focus_attempt(_source=None) -> bool:
            if _omni_is_visible():
                return GLib.SOURCE_REMOVE
            try:
                terminal.grab_terminal_focus()
            except Exception as focus_error:
                logger.debug(f"Deferred terminal focus failed: {focus_error}")
            return GLib.SOURCE_REMOVE

        # Try immediate focus
        try:
            if _omni_is_visible():
                return
            terminal.grab_terminal_focus()
        except Exception:
            pass

        # Schedule retries for delayed focus (useful when widget is still being created)
        GLib.idle_add(_focus_attempt, priority=GLib.PRIORITY_DEFAULT_IDLE)
        GLib.timeout_add(150, _focus_attempt)
        GLib.timeout_add(350, _focus_attempt)

    def _get_active_terminal_widget(self) -> Optional[TerminalWidget]:
        """Return the TerminalWidget for the currently selected tab, if any."""
        terminal_manager = getattr(self, 'terminal_manager', None)
        if terminal_manager is not None:
            return terminal_manager.get_focused_terminal()
        return None

    def on_tab_selected(self, tab_view: Adw.TabView, _pspec=None) -> None:
        """Update active terminal mapping when the user switches tabs."""
        self._update_content_theme_for_selected_tab()
        self._update_layout_toggle_state()
        try:
            page = tab_view.get_selected_page()
            if page is None:
                return
            child = page.get_child() if hasattr(page, 'get_child') else None
            if child is None:
                return

            if self._is_start_tab_page(page):
                GLib.idle_add(self._focus_connection_list_first_row)
                try:
                    if not self.has_user_tabs():
                        # Back at the welcome screen with no sessions: undo a
                        # transient minimize-on-connect, but keep the strip when
                        # minimal is the configured resting mode.
                        if (getattr(self, '_sidebar_minimal', False)
                                and not self._sidebar_mode_is_minimal()):
                            self.set_sidebar_minimal(False)
                        if self.config.get_setting('ui.sidebar_show_when_no_tabs', False):
                            self._apply_sidebar_visible(True)
                except Exception:
                    pass
                return
            
            # Focus the terminal when tab is selected
            if _is_terminal_widget(child):
                # Use a small delay to ensure the widget is fully visible
                def _focus_on_tab_switch():
                    try:
                        self._focus_terminal_widget(child)
                    except Exception as e:
                        logger.debug(f"Failed to focus terminal on tab switch: {e}")
                GLib.timeout_add(50, _focus_on_tab_switch)

            # Split-view tabs have no single connection — clear sidebar selection
            from .split_view import SplitViewTab
            if isinstance(child, SplitViewTab):
                try:
                    if hasattr(self.connection_list, 'unselect_all'):
                        self.connection_list.unselect_all()
                    else:
                        current = self.connection_list.get_selected_row()
                        if current is not None:
                            self.connection_list.unselect_row(current)
                except Exception:
                    pass
                return

            connection = self.terminal_to_connection.get(child)
            if connection:
                # Check if this is a local terminal
                host_value = _get_connection_host(connection) or _get_connection_alias(connection)
                if host_value == 'localhost':
                    # Local terminal - clear selection
                    try:
                        if hasattr(self.connection_list, 'unselect_all'):
                            self.connection_list.unselect_all()
                        else:
                            current = self.connection_list.get_selected_row()
                            if current is not None:
                                self.connection_list.unselect_row(current)
                    except Exception:
                        pass
                else:
                    # Regular connection terminal - select the corresponding row
                    self.active_terminals[connection] = child
                    conn_rows = self._rows_for_connection(connection)
                    if conn_rows:
                        selected_rows = []
                        try:
                            selected_rows = list(self.connection_list.get_selected_rows())
                        except Exception:
                            current = self.connection_list.get_selected_row()
                            if current:
                                selected_rows = [current]
                        # Leave the selection alone if any row for this
                        # connection is already selected; otherwise select the first.
                        if not any(r in selected_rows for r in conn_rows):
                            self._select_only_row(conn_rows[0])
            else:
                # Other non-connection terminal - clear selection
                try:
                    if hasattr(self.connection_list, 'unselect_all'):
                        self.connection_list.unselect_all()
                    else:
                        current = self.connection_list.get_selected_row()
                        if current is not None:
                            self.connection_list.unselect_row(current)
                except Exception:
                    pass
        except Exception as e:
            logger.error(f"Failed to sync tab selection: {e}")

    def _update_tab_titles(self):
        """Update tab titles"""
        for page in self.tab_view.get_pages():
            if self._is_start_tab_page(page):
                continue
            child = page.get_child()
            if hasattr(child, 'connection'):
                page.set_title(child.connection.nickname)


def _is_terminal_widget(widget) -> bool:
    from .terminal import TerminalWidget
    return isinstance(widget, TerminalWidget)
