"""Hermes Hub — Add Account Multi-Step Wizard Modal (v4).

Supports 5 AI Providers:
1. Google Antigravity (Cockpit Tools model OAuth with immediate URL & manual fallback)
2. OpenAI Codex (OAuth / ChatGPT Device Code flow + OpenAI API Key mode)
3. OpenCode Go (API Key / Token input with clipboard Paste button & full keyboard shortcuts)
4. Claude / Anthropic (OAuth PKCE for Claude Pro/Max + Anthropic API Key mode)
5. Grok / xAI (OAuth Device Code for SuperGrok + xAI API Key mode)

Includes multi-account profile isolation, duplicate detection, and auto-assignment to router roles.
"""

from __future__ import annotations

import json
import os
import threading
import time
import webbrowser
from typing import Any, Callable, Dict, List, Optional
import customtkinter as ctk

from antigravity_provider.router.ui.theme import Theme
from antigravity_provider.router.ui.components import HubButton, HubCard, HubEntry, HubModal
from antigravity_provider.router.auto_assigner import AutoAssigner
from antigravity_provider.router.profile_manager import ProfileAuthManager
from antigravity_provider.router.router_config import load_router_config
from antigravity_provider.router.unified_health import EventLogService


def ensure_profile_in_routing(profile_id: str) -> tuple[bool, str]:
    """Keep existing chain rank or route a newly introduced profile slot."""
    config = load_router_config()
    assigned_role = next(
        (role_id for role_id, policy in config.roles.items() if profile_id in policy.preferred_chain),
        "",
    )
    if assigned_role:
        return True, f"Профиль уже входит в цепочку '{assigned_role}'"
    _display_name, role_code, tier = AutoAssigner.get_display_name_and_role(profile_id)
    return AutoAssigner.assign_profile_to_role(profile_id, role_code, is_primary=tier == "primary")


class AddAccountWizard(HubModal):
    """4-Step Add Account Wizard with OAuth / API Key support and Auto-Assignment."""

    def __init__(self, parent: Any, on_complete: Optional[Callable[[Dict[str, Any]], None]] = None):
        super().__init__(parent, title="Мастер подключения аккаунта", width=640, height=560)
        self.on_complete = on_complete
        self.step = 1

        self.selected_provider: str = "antigravity"
        self.codex_auth_mode: str = "oauth"  # oauth | api_key
        self.claude_auth_mode: str = "oauth"  # oauth | api_key
        self.grok_auth_mode: str = "oauth"  # oauth | api_key

        self.target_slot: str = ""
        self.discovered_identity: str = ""
        self.discovered_plan: str = "Тариф: неизвестен"
        self.discovered_models: List[str] = []
        self.is_verified: bool = False

        # Session tracking
        self.oauth_session_id: Optional[str] = None
        self.oauth_url: Optional[str] = None
        self.oauth_port: Optional[int] = None

        self.codex_session_id: Optional[str] = None
        self.codex_url: Optional[str] = None
        self.codex_user_code: Optional[str] = None

        self.claude_session_id: Optional[str] = None
        self.claude_url: Optional[str] = None

        self.grok_session_id: Optional[str] = None
        self.grok_url: Optional[str] = None
        self.grok_user_code: Optional[str] = None

        self._polling_active = False

        self._show_step_1_provider()

    def destroy(self):
        self._polling_active = False
        self._cancel_active_sessions()
        super().destroy()

    def _clear_body(self):
        for w in self.body.winfo_children():
            w.destroy()
        for w in self.footer.winfo_children():
            w.destroy()

    def _cancel_active_sessions(self):
        if self.oauth_session_id:
            try:
                from antigravity_provider.router.profile_oauth import cancel_oauth_session

                cancel_oauth_session(self.oauth_session_id)
            except Exception:
                pass
            self.oauth_session_id = None
            self.oauth_port = None

        if self.codex_session_id:
            try:
                from antigravity_provider.router.codex_oauth import cancel_codex_oauth_session

                cancel_codex_oauth_session(self.codex_session_id)
            except Exception:
                pass
            self.codex_session_id = None

        if self.claude_session_id:
            try:
                from antigravity_provider.router.claude_oauth import cancel_claude_oauth_session

                cancel_claude_oauth_session(self.claude_session_id)
            except Exception:
                pass
            self.claude_session_id = None

        if self.grok_session_id:
            try:
                from antigravity_provider.router.grok_oauth import cancel_grok_oauth_session

                cancel_grok_oauth_session(self.grok_session_id)
            except Exception:
                pass
            self.grok_session_id = None

    # ═══════════════════════════════════════════════════════════════
    #  STEP 1: Provider Selection
    # ═══════════════════════════════════════════════════════════════

    def _show_step_1_provider(self):
        self._cancel_active_sessions()
        self._polling_active = False
        self._clear_body()
        self.title_lbl.configure(text="Шаг 1 из 4: Выберите провайдера ИИ")

        ctk.CTkLabel(
            self.body,
            text="Выберите платформу для подключения учетной записи:",
            font=Theme.font_body(),
            text_color=Theme.TEXT_SECONDARY,
        ).pack(anchor="w", pady=(0, 10))

        providers = [
            ("antigravity", "Google Antigravity", "OAuth 2.0 (Google Account)", Theme.ACCENT),
            ("openai-codex", "OpenAI Codex", "OAuth (ChatGPT) или API Key", Theme.PROVIDER_CODEX),
            ("claude", "Claude (Anthropic)", "OAuth (Claude Pro/Max) или API Key", Theme.PROVIDER_CLAUDE),
            ("grok", "Grok (xAI)", "OAuth (SuperGrok) или API Key", Theme.PROVIDER_GROK),
            ("opencode-go", "OpenCode Go", "API Key / Subscription", Theme.PROVIDER_GENERIC),
        ]

        self.provider_var = ctk.StringVar(value=self.selected_provider)

        for p_id, p_title, p_desc, p_color in providers:
            card = HubCard(self.body)
            card.pack(fill="x", pady=3)

            r_frame = ctk.CTkFrame(card, fg_color="transparent")
            r_frame.pack(fill="x", padx=12, pady=6)

            rb = ctk.CTkRadioButton(
                r_frame,
                text=p_title,
                value=p_id,
                variable=self.provider_var,
                font=Theme.font_body_bold(),
                fg_color=p_color,
                command=lambda pid=p_id: self._select_provider(pid),
            )
            rb.pack(side="left")

            ctk.CTkLabel(
                r_frame,
                text=f"•  {p_desc}",
                font=Theme.font_caption(),
                text_color=Theme.TEXT_MUTED,
            ).pack(side="left", padx=(10, 0))

        # Footer
        HubButton(
            self.footer,
            text="Отмена",
            variant="ghost",
            command=self.destroy,
        ).pack(side="left", padx=10, pady=10)

        HubButton(
            self.footer,
            text="Далее →",
            variant="primary",
            command=self._proceed_to_step_2,
        ).pack(side="right", padx=10, pady=10)

    def _select_provider(self, p_id: str):
        self.selected_provider = p_id

    def _proceed_to_step_2(self):
        self.selected_provider = self.provider_var.get()
        self.step = 2
        self._show_step_2_auth()

    # ═══════════════════════════════════════════════════════════════
    #  STEP 2: Authentication
    # ═══════════════════════════════════════════════════════════════

    def _show_step_2_auth(self):
        self._clear_body()
        self.title_lbl.configure(text="Шаг 2 из 4: Авторизация учетной записи")

        self.target_slot = AutoAssigner.find_free_slot(self.selected_provider) or ""
        if not self.target_slot:
            self._show_no_free_slot()
            return

        if self.selected_provider == "antigravity":
            self._build_antigravity_oauth_flow()
        elif self.selected_provider == "openai-codex":
            if self.codex_auth_mode == "oauth":
                self._build_codex_oauth_flow()
            else:
                self._build_api_key_flow()
        elif self.selected_provider == "claude":
            if self.claude_auth_mode == "oauth":
                self._build_claude_oauth_flow()
            else:
                self._build_api_key_flow()
        elif self.selected_provider == "grok":
            if self.grok_auth_mode == "oauth":
                self._build_grok_oauth_flow()
            else:
                self._build_api_key_flow()
        else:
            self._build_api_key_flow()

    def _show_no_free_slot(self) -> None:
        """Stop before OAuth when the configured provider has no real free slot."""
        provider_labels = {
            "antigravity": "Google Antigravity",
            "openai-codex": "OpenAI Codex",
            "opencode-go": "OpenCode Go",
            "claude": "Claude",
            "grok": "Grok",
        }
        card = HubCard(self.body, border_color=Theme.STATUS_WARNING, fg_color=Theme.SURFACE_MUTED)
        card.pack(fill="x", pady=20)
        ctk.CTkLabel(
            card,
            text="Нет свободного слота",
            font=Theme.font_heading(),
            text_color=Theme.STATUS_WARNING,
        ).pack(anchor="w", padx=16, pady=(14, 6))
        ctk.CTkLabel(
            card,
            text=(
                f"Все слоты провайдера {provider_labels.get(self.selected_provider, self.selected_provider)} заняты.\n"
                "Освободите один слот или удалите неиспользуемый аккаунт, затем повторите подключение."
            ),
            font=Theme.font_body(),
            text_color=Theme.TEXT_PRIMARY,
            justify="left",
            wraplength=520,
        ).pack(anchor="w", padx=16, pady=(0, 14))
        self._build_step_2_footer()

    # ─────────────────────────────────────────────────────────────
    #  GOOGLE ANTIGRAVITY OAUTH FLOW
    # ─────────────────────────────────────────────────────────────

    def _build_antigravity_oauth_flow(self):
        ctk.CTkLabel(
            self.body,
            text="Для Google Antigravity требуется авторизация Google OAuth.",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
            anchor="w",
        ).pack(fill="x", pady=(0, 2))

        # Authorization link card
        auth_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
        auth_card.pack(fill="x", pady=(0, 8))

        ctk.CTkLabel(
            auth_card,
            text="Ссылка авторизации",
            font=Theme.font_caption(),
            text_color=Theme.TEXT_SECONDARY,
        ).pack(anchor="w", padx=10, pady=(6, 2))

        url_row = ctk.CTkFrame(auth_card, fg_color="transparent")
        url_row.pack(fill="x", padx=10, pady=(0, 6))

        self.oauth_url_entry = HubEntry(
            url_row,
            font=Theme.font_mono(),
            height=32,
            fg_color=Theme.PRIMARY,
            border_color=Theme.BORDER,
            text_color=Theme.TEXT_PRIMARY,
        )
        self.oauth_url_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))

        self.copy_url_btn = HubButton(
            url_row,
            text="📋",
            variant="secondary",
            width=40,
            height=32,
            command=self._copy_oauth_url,
        )
        self.copy_url_btn.pack(side="right")

        action_row = ctk.CTkFrame(auth_card, fg_color="transparent")
        action_row.pack(fill="x", padx=10, pady=(0, 8))

        self.open_browser_btn = HubButton(
            action_row,
            text="🌐 Открыть в браузере",
            variant="primary",
            width=180,
            command=self._open_oauth_browser,
        )
        self.open_browser_btn.pack(side="left", padx=(0, 8))

        # Manual Callback Fallback Card
        manual_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
        manual_card.pack(fill="x", pady=(0, 8))

        ctk.CTkLabel(
            manual_card,
            text="▸ Не удалось завершить авторизацию автоматически?",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
            anchor="w",
        ).pack(fill="x", padx=10, pady=(6, 2))

        ctk.CTkLabel(
            manual_card,
            text="Вставьте полный URL из адресной строки браузера (если localhost вернул ошибку):",
            font=Theme.font_caption(),
            text_color=Theme.TEXT_SECONDARY,
            anchor="w",
        ).pack(fill="x", padx=10, pady=(0, 4))

        manual_entry_row = ctk.CTkFrame(manual_card, fg_color="transparent")
        manual_entry_row.pack(fill="x", padx=10, pady=(0, 6))

        self.manual_callback_entry = HubEntry(
            manual_entry_row,
            placeholder_text="http://127.0.0.1:49725/oauth-callback?state=...&code=...",
            font=Theme.font_mono(),
            height=32,
            fg_color=Theme.PRIMARY,
            border_color=Theme.BORDER,
            text_color=Theme.TEXT_PRIMARY,
        )
        self.manual_callback_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))

        HubButton(
            manual_entry_row,
            text="📋 Вставить",
            variant="secondary",
            width=80,
            height=32,
            command=lambda: self._paste_into_entry(self.manual_callback_entry),
        ).pack(side="right")

        self.manual_submit_btn = HubButton(
            manual_card,
            text="✓ Завершить авторизацию",
            variant="secondary",
            height=30,
            command=self._handle_manual_callback_submit,
        )
        self.manual_submit_btn.pack(anchor="w", padx=10, pady=(0, 6))

        # Status text
        self.oauth_status_lbl = ctk.CTkLabel(
            self.body,
            text="Создание сессии авторизации...",
            font=Theme.font_body(),
            text_color=Theme.TEXT_SECONDARY,
            anchor="w",
        )
        self.oauth_status_lbl.pack(fill="x", pady=4)

        self._build_step_2_footer()
        self._init_antigravity_oauth()

    def _init_antigravity_oauth(self):
        try:
            from antigravity_provider.router.profile_oauth import start_profile_oauth

            session_id, auth_url, port = start_profile_oauth(self.target_slot)
            self.oauth_session_id = session_id
            self.oauth_url = auth_url
            self.oauth_port = port

            self.oauth_url_entry.delete(0, "end")
            self.oauth_url_entry.insert(0, auth_url)

            self.oauth_status_lbl.configure(
                text="Ожидание завершения авторизации в браузере...",
                text_color=Theme.TEXT_SECONDARY,
            )

            self._polling_active = True
            threading.Thread(target=self._poll_antigravity_oauth, daemon=True).start()
        except Exception as e:
            self.oauth_status_lbl.configure(
                text=f"Ошибка создания сессии: {e}",
                text_color=Theme.STATUS_ERROR,
            )

    def _copy_oauth_url(self):
        if self.oauth_url:
            self.clipboard_clear()
            self.clipboard_append(self.oauth_url)
            self.copy_url_btn.configure(text="✓")
            self.after(2000, lambda: self.copy_url_btn.configure(text="📋"))

    def _open_oauth_browser(self):
        if self.oauth_url:
            webbrowser.open(self.oauth_url)

    def _handle_manual_callback_submit(self):
        raw_url = self.manual_callback_entry.get().strip()
        if not raw_url:
            self.oauth_status_lbl.configure(text="❌ Вставьте полный URL callback", text_color=Theme.STATUS_ERROR)
            return

        from antigravity_provider.router.profile_oauth import get_oauth_session

        session = get_oauth_session(self.oauth_session_id)
        if not session:
            self.oauth_status_lbl.configure(text="❌ Сессия не найдена", text_color=Theme.STATUS_ERROR)
            return

        ok, msg = session.handle_manual_callback_url(raw_url)
        if ok:
            info = getattr(session, "completed_profile_info", {}) or {}
            self.discovered_identity = info.get("email") or "Google Account"
            self.discovered_plan = "PRO"
            self.discovered_models = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-thinking"]
            self.is_verified = True
            self._show_step_3_validation()
        else:
            self.oauth_status_lbl.configure(text=f"❌ {msg}", text_color=Theme.STATUS_ERROR)

    def _poll_antigravity_oauth(self):
        from antigravity_provider.router.profile_oauth import get_oauth_session

        for _ in range(300):
            if not self._polling_active:
                return
            time.sleep(1)
            session = get_oauth_session(self.oauth_session_id)
            if not session:
                continue

            status = getattr(session, "status", "").lower()
            if status in ("completed", "success"):
                info = getattr(session, "completed_profile_info", {}) or {}
                self.discovered_identity = info.get("email") or "Google Account"
                self.discovered_plan = "PRO"
                self.discovered_models = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-thinking"]
                self.is_verified = True
                self.after(0, self._show_step_3_validation)
                return
            elif status in ("error", "failed", "cancelled"):
                err_msg = getattr(session, "error_msg", None) or "Авторизация не удалась"
                self.after(
                    0, lambda m=err_msg: self.oauth_status_lbl.configure(text=f"❌ {m}", text_color=Theme.STATUS_ERROR)
                )
                return

    # ─────────────────────────────────────────────────────────────
    #  OPENAI CODEX OAUTH FLOW
    # ─────────────────────────────────────────────────────────────

    def _build_codex_oauth_flow(self):
        mode_card = HubCard(self.body, fg_color="transparent")
        mode_card.pack(fill="x", pady=(0, 8))

        self.codex_mode_var = ctk.StringVar(value="oauth")

        rb1 = ctk.CTkRadioButton(
            mode_card,
            text="OAuth — OpenAI / ChatGPT аккаунт (Рекомендуется)",
            value="oauth",
            variable=self.codex_mode_var,
            font=Theme.font_body_bold(),
            fg_color=Theme.ACCENT,
            command=self._on_codex_mode_toggle,
        )
        rb1.pack(anchor="w", padx=4, pady=2)

        rb2 = ctk.CTkRadioButton(
            mode_card,
            text="API Key — OpenAI API (sk-...)",
            value="api_key",
            variable=self.codex_mode_var,
            font=Theme.font_body(),
            fg_color=Theme.ACCENT,
            command=self._on_codex_mode_toggle,
        )
        rb2.pack(anchor="w", padx=4, pady=2)

        auth_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
        auth_card.pack(fill="x", pady=(0, 8))

        ctk.CTkLabel(
            auth_card,
            text="1. Откройте ссылку — кнопка ниже откроет страницу OpenAI",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
        ).pack(anchor="w", padx=10, pady=(8, 2))

        url_row = ctk.CTkFrame(auth_card, fg_color="transparent")
        url_row.pack(fill="x", padx=10, pady=(0, 6))

        self.codex_url_entry = HubEntry(
            url_row,
            font=Theme.font_mono(),
            height=32,
            fg_color=Theme.PRIMARY,
            border_color=Theme.BORDER,
            text_color=Theme.TEXT_PRIMARY,
        )
        self.codex_url_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))

        HubButton(
            url_row,
            text="Копировать ссылку",
            variant="secondary",
            width=130,
            height=32,
            command=self._copy_codex_url,
        ).pack(side="right")

        code_card = ctk.CTkFrame(auth_card, fg_color="transparent")
        code_card.pack(fill="x", padx=10, pady=(0, 6))

        ctk.CTkLabel(
            code_card,
            text="2. Введите на странице код:",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
        ).pack(side="left", padx=(0, 8))

        self.codex_code_lbl = ctk.CTkLabel(
            code_card,
            text="...",
            font=("Consolas", 18, "bold"),
            text_color=Theme.ACCENT,
        )
        self.codex_code_lbl.pack(side="left", padx=(0, 8))

        HubButton(
            code_card,
            text="📋 Копировать код",
            variant="secondary",
            width=130,
            height=28,
            command=self._copy_codex_code,
        ).pack(side="left")

        action_row = ctk.CTkFrame(auth_card, fg_color="transparent")
        action_row.pack(fill="x", padx=10, pady=(0, 8))

        HubButton(
            action_row,
            text="🌐 Открыть в браузере",
            variant="primary",
            width=180,
            command=self._open_codex_browser,
        ).pack(side="left", padx=(0, 8))

        ctk.CTkLabel(
            auth_card,
            text="3. Подтвердите доступ — мастер продолжит автоматически",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
        ).pack(anchor="w", padx=10, pady=(0, 8))

        # Manual Fallback Card
        manual_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
        manual_card.pack(fill="x", pady=(0, 8))

        ctk.CTkLabel(
            manual_card,
            text="▸ Вставить токен вручную:",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
            anchor="w",
        ).pack(fill="x", padx=10, pady=(6, 2))

        manual_entry_row = ctk.CTkFrame(manual_card, fg_color="transparent")
        manual_entry_row.pack(fill="x", padx=10, pady=(0, 6))

        self.codex_manual_entry = HubEntry(
            manual_entry_row,
            placeholder_text='{"access_token": "..."} или токен...',
            font=Theme.font_mono(),
            height=32,
            fg_color=Theme.PRIMARY,
            border_color=Theme.BORDER,
            text_color=Theme.TEXT_PRIMARY,
        )
        self.codex_manual_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))

        HubButton(
            manual_entry_row,
            text="📋 Вставить",
            variant="secondary",
            width=80,
            height=32,
            command=lambda: self._paste_into_entry(self.codex_manual_entry),
        ).pack(side="right")

        HubButton(
            manual_card,
            text="✓ Завершить авторизацию",
            variant="secondary",
            height=30,
            command=self._handle_codex_manual_submit,
        ).pack(anchor="w", padx=10, pady=(0, 6))

        self.codex_status_lbl = ctk.CTkLabel(
            self.body,
            text="Создание сессии входа OpenAI...",
            font=Theme.font_body(),
            text_color=Theme.TEXT_SECONDARY,
            anchor="w",
        )
        self.codex_status_lbl.pack(fill="x", pady=4)

        self._build_step_2_footer()
        self._init_codex_oauth()

    def _on_codex_mode_toggle(self):
        self.codex_auth_mode = self.codex_mode_var.get()
        self._show_step_2_auth()

    def _init_codex_oauth(self):
        try:
            from antigravity_provider.router.codex_oauth import start_codex_oauth, get_codex_oauth_session

            session_id, url, code = start_codex_oauth(self.target_slot)
            self.codex_session_id = session_id
            self.codex_url = url
            self.codex_user_code = code

            session = get_codex_oauth_session(session_id)
            if not code or (session and session.status == "failed"):
                err_msg = getattr(session, "error_msg", None) or "Не удалось получить код авторизации"
                self.codex_status_lbl.configure(text=f"❌ {err_msg}", text_color=Theme.STATUS_ERROR)
                return

            self.codex_url_entry.delete(0, "end")
            self.codex_url_entry.insert(0, url)
            self.codex_code_lbl.configure(text=code)

            if getattr(session, "is_dev_mode", False):
                self.codex_status_lbl.configure(
                    text=f"⚠️ ТЕСТОВЫЙ РЕЖИМ (HERMES_HUB_DEV_MODE): Код {code}",
                    text_color=Theme.STATUS_WARNING,
                )
            else:
                self.codex_status_lbl.configure(
                    text=f"Ожидание подтверждения кода {code} в браузере...",
                    text_color=Theme.TEXT_SECONDARY,
                )

            self._polling_active = True
            threading.Thread(target=self._poll_codex_oauth, daemon=True).start()
        except Exception as e:
            self.codex_status_lbl.configure(text=f"Ошибка: {e}", text_color=Theme.STATUS_ERROR)

    def _copy_codex_url(self):
        if self.codex_url:
            self.clipboard_clear()
            self.clipboard_append(self.codex_url)

    def _copy_codex_code(self):
        if self.codex_user_code:
            self.clipboard_clear()
            self.clipboard_append(self.codex_user_code)

    def _open_codex_browser(self):
        if self.codex_url:
            self._copy_codex_code()
            self.codex_status_lbl.configure(
                text=(
                    "Код скопирован. Вставьте его на открывшейся странице OpenAI; "
                    "затем вернитесь сюда — Hub продолжит автоматически."
                ),
                text_color=Theme.TEXT_SECONDARY,
            )
            webbrowser.open(self.codex_url)

    def _handle_codex_manual_submit(self):
        raw = self.codex_manual_entry.get().strip()
        if not raw:
            self.codex_status_lbl.configure(text="❌ Введите данные токена", text_color=Theme.STATUS_ERROR)
            return

        from antigravity_provider.router.codex_oauth import get_codex_oauth_session

        session = get_codex_oauth_session(self.codex_session_id)
        if not session:
            self.codex_status_lbl.configure(text="❌ Сессия не найдена", text_color=Theme.STATUS_ERROR)
            return

        ok, msg = session.handle_manual_input(raw)
        if ok:
            info = getattr(session, "completed_profile_info", {}) or {}
            self.discovered_identity = info.get("email") or "ChatGPT Account"
            self.discovered_plan = "PLUS"
            self.discovered_models = ["gpt-4o", "o3-mini", "gpt-4o-mini", "codex"]
            self.is_verified = True
            self._show_step_3_validation()
        else:
            self.codex_status_lbl.configure(text=f"❌ {msg}", text_color=Theme.STATUS_ERROR)

    def _poll_codex_oauth(self):
        from antigravity_provider.router.codex_oauth import get_codex_oauth_session

        for _ in range(900):
            if not self._polling_active:
                return
            time.sleep(1)
            session = get_codex_oauth_session(self.codex_session_id)
            if not session:
                continue

            status = getattr(session, "status", "").lower()
            if status in ("completed", "success"):
                info = getattr(session, "completed_profile_info", {}) or {}
                self.discovered_identity = info.get("email") or "ChatGPT Account"
                self.discovered_plan = "PLUS"
                self.discovered_models = ["gpt-4o", "o3-mini", "gpt-4o-mini", "codex"]
                self.is_verified = True
                self.after(0, self._show_step_3_validation)
                return

    # ─────────────────────────────────────────────────────────────
    #  CLAUDE (ANTHROPIC) OAUTH FLOW
    # ─────────────────────────────────────────────────────────────

    def _build_claude_oauth_flow(self):
        mode_card = HubCard(self.body, fg_color="transparent")
        mode_card.pack(fill="x", pady=(0, 8))

        self.claude_mode_var = ctk.StringVar(value="oauth")

        rb1 = ctk.CTkRadioButton(
            mode_card,
            text="OAuth — Claude Pro/Max Account (Рекомендуется)",
            value="oauth",
            variable=self.claude_mode_var,
            font=Theme.font_body_bold(),
            fg_color=Theme.PROVIDER_CLAUDE,
            command=self._on_claude_mode_toggle,
        )
        rb1.pack(anchor="w", padx=4, pady=2)

        rb2 = ctk.CTkRadioButton(
            mode_card,
            text="API Key — Anthropic API (sk-ant-...)",
            value="api_key",
            variable=self.claude_mode_var,
            font=Theme.font_body(),
            fg_color=Theme.PROVIDER_CLAUDE,
            command=self._on_claude_mode_toggle,
        )
        rb2.pack(anchor="w", padx=4, pady=2)

        auth_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
        auth_card.pack(fill="x", pady=(0, 8))

        ctk.CTkLabel(
            auth_card,
            text="Ссылка для входа в Claude:",
            font=Theme.font_caption(),
            text_color=Theme.TEXT_SECONDARY,
        ).pack(anchor="w", padx=10, pady=(6, 2))

        url_row = ctk.CTkFrame(auth_card, fg_color="transparent")
        url_row.pack(fill="x", padx=10, pady=(0, 6))

        self.claude_url_entry = HubEntry(
            url_row,
            font=Theme.font_mono(),
            height=32,
            fg_color=Theme.PRIMARY,
            border_color=Theme.BORDER,
            text_color=Theme.TEXT_PRIMARY,
        )
        self.claude_url_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))

        HubButton(
            url_row,
            text="Копировать ссылку",
            variant="secondary",
            width=130,
            height=32,
            command=self._copy_claude_url,
        ).pack(side="right")

        action_row = ctk.CTkFrame(auth_card, fg_color="transparent")
        action_row.pack(fill="x", padx=10, pady=(0, 8))

        HubButton(
            action_row,
            text="🌐 Открыть в браузере",
            variant="primary",
            width=180,
            command=self._open_claude_browser,
        ).pack(side="left", padx=(0, 8))

        # Code Entry Card
        code_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
        code_card.pack(fill="x", pady=(0, 8))

        ctk.CTkLabel(
            code_card,
            text="Вставьте код авторизации после входа на сайте Claude:",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
            anchor="w",
        ).pack(fill="x", padx=10, pady=(6, 2))

        code_row = ctk.CTkFrame(code_card, fg_color="transparent")
        code_row.pack(fill="x", padx=10, pady=(0, 6))

        self.claude_code_entry = HubEntry(
            code_row,
            placeholder_text="Вставьте код...",
            font=Theme.font_mono(),
            height=32,
            fg_color=Theme.PRIMARY,
            border_color=Theme.BORDER,
            text_color=Theme.TEXT_PRIMARY,
        )
        self.claude_code_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))

        HubButton(
            code_row,
            text="📋 Вставить",
            variant="secondary",
            width=80,
            height=32,
            command=lambda: self._paste_into_entry(self.claude_code_entry),
        ).pack(side="right")

        HubButton(
            code_card,
            text="✓ Завершить авторизацию",
            variant="secondary",
            height=30,
            command=self._handle_claude_code_submit,
        ).pack(anchor="w", padx=10, pady=(0, 6))

        self.claude_status_lbl = ctk.CTkLabel(
            self.body,
            text="Создание ссылки входа Claude...",
            font=Theme.font_body(),
            text_color=Theme.TEXT_SECONDARY,
            anchor="w",
        )
        self.claude_status_lbl.pack(fill="x", pady=4)

        self._build_step_2_footer()
        self._init_claude_oauth()

    def _on_claude_mode_toggle(self):
        self.claude_auth_mode = self.claude_mode_var.get()
        self._show_step_2_auth()

    def _init_claude_oauth(self):
        try:
            from antigravity_provider.router.claude_oauth import start_claude_oauth

            session_id, url = start_claude_oauth(self.target_slot)
            self.claude_session_id = session_id
            self.claude_url = url

            self.claude_url_entry.delete(0, "end")
            self.claude_url_entry.insert(0, url)
            self.claude_status_lbl.configure(text="Перейдите по ссылке и скопируйте код авторизации.")
        except Exception as e:
            self.claude_status_lbl.configure(text=f"Ошибка: {e}", text_color=Theme.STATUS_ERROR)

    def _copy_claude_url(self):
        if self.claude_url:
            self.clipboard_clear()
            self.clipboard_append(self.claude_url)

    def _open_claude_browser(self):
        if self.claude_url:
            webbrowser.open(self.claude_url)

    def _handle_claude_code_submit(self):
        raw = self.claude_code_entry.get().strip()
        if not raw:
            self.claude_status_lbl.configure(text="❌ Вставьте код авторизации", text_color=Theme.STATUS_ERROR)
            return

        from antigravity_provider.router.claude_oauth import get_claude_oauth_session

        session = get_claude_oauth_session(self.claude_session_id)
        if not session:
            self.claude_status_lbl.configure(text="❌ Сессия не найдена", text_color=Theme.STATUS_ERROR)
            return

        ok, msg = session.handle_auth_code(raw)
        if ok:
            info = getattr(session, "completed_profile_info", {}) or {}
            self.discovered_identity = info.get("email") or "Claude Account"
            self.discovered_plan = "MAX"
            self.discovered_models = ["claude-3-7-sonnet", "claude-3-5-sonnet", "claude-3-5-haiku", "claude-3-opus"]
            self.is_verified = True
            self._show_step_3_validation()
        else:
            self.claude_status_lbl.configure(text=f"❌ {msg}", text_color=Theme.STATUS_ERROR)

    # ─────────────────────────────────────────────────────────────
    #  GROK (XAI) OAUTH FLOW
    # ─────────────────────────────────────────────────────────────

    def _build_grok_oauth_flow(self):
        mode_card = HubCard(self.body, fg_color="transparent")
        mode_card.pack(fill="x", pady=(0, 8))

        self.grok_mode_var = ctk.StringVar(value="oauth")

        rb1 = ctk.CTkRadioButton(
            mode_card,
            text="OAuth — xAI / SuperGrok Account (Рекомендуется)",
            value="oauth",
            variable=self.grok_mode_var,
            font=Theme.font_body_bold(),
            fg_color=Theme.PROVIDER_GROK,
            command=self._on_grok_mode_toggle,
        )
        rb1.pack(anchor="w", padx=4, pady=2)

        rb2 = ctk.CTkRadioButton(
            mode_card,
            text="API Key — xAI API (xai-...)",
            value="api_key",
            variable=self.grok_mode_var,
            font=Theme.font_body(),
            fg_color=Theme.PROVIDER_GROK,
            command=self._on_grok_mode_toggle,
        )
        rb2.pack(anchor="w", padx=4, pady=2)

        auth_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
        auth_card.pack(fill="x", pady=(0, 8))

        ctk.CTkLabel(
            auth_card,
            text="1. Откройте ссылку — кнопка ниже откроет страницу xAI",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
        ).pack(anchor="w", padx=10, pady=(8, 2))

        url_row = ctk.CTkFrame(auth_card, fg_color="transparent")
        url_row.pack(fill="x", padx=10, pady=(0, 6))

        self.grok_url_entry = HubEntry(
            url_row,
            font=Theme.font_mono(),
            height=32,
            fg_color=Theme.PRIMARY,
            border_color=Theme.BORDER,
            text_color=Theme.TEXT_PRIMARY,
        )
        self.grok_url_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))

        HubButton(
            url_row,
            text="📋",
            variant="secondary",
            width=40,
            height=32,
            command=self._copy_grok_url,
        ).pack(side="right")

        code_card = ctk.CTkFrame(auth_card, fg_color="transparent")
        code_card.pack(fill="x", padx=10, pady=(0, 6))

        ctk.CTkLabel(
            code_card,
            text="2. Введите на странице код:",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
        ).pack(side="left", padx=(0, 8))

        self.grok_code_lbl = ctk.CTkLabel(
            code_card,
            text="...",
            font=("Consolas", 18, "bold"),
            text_color=Theme.PROVIDER_GROK,
        )
        self.grok_code_lbl.pack(side="left", padx=(0, 8))

        HubButton(
            code_card,
            text="📋 Копировать код",
            variant="secondary",
            width=130,
            height=28,
            command=self._copy_grok_code,
        ).pack(side="left")

        action_row = ctk.CTkFrame(auth_card, fg_color="transparent")
        action_row.pack(fill="x", padx=10, pady=(0, 8))

        HubButton(
            action_row,
            text="🌐 Открыть в браузере",
            variant="primary",
            width=180,
            command=self._open_grok_browser,
        ).pack(side="left", padx=(0, 8))

        ctk.CTkLabel(
            auth_card,
            text="3. Подтвердите доступ — мастер продолжит автоматически",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
        ).pack(anchor="w", padx=10, pady=(0, 8))

        # Manual Entry Card
        manual_card = HubCard(self.body, fg_color=Theme.SURFACE_MUTED)
        manual_card.pack(fill="x", pady=(0, 8))

        ctk.CTkLabel(
            manual_card,
            text="▸ Вставить токен вручную:",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
            anchor="w",
        ).pack(fill="x", padx=10, pady=(6, 2))

        manual_entry_row = ctk.CTkFrame(manual_card, fg_color="transparent")
        manual_entry_row.pack(fill="x", padx=10, pady=(0, 6))

        self.grok_manual_entry = HubEntry(
            manual_entry_row,
            placeholder_text='{"access_token": "..."} или токен...',
            font=Theme.font_mono(),
            height=32,
            fg_color=Theme.PRIMARY,
            border_color=Theme.BORDER,
            text_color=Theme.TEXT_PRIMARY,
        )
        self.grok_manual_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))

        HubButton(
            manual_entry_row,
            text="📋 Вставить",
            variant="secondary",
            width=80,
            height=32,
            command=lambda: self._paste_into_entry(self.grok_manual_entry),
        ).pack(side="right")

        HubButton(
            manual_card,
            text="✓ Завершить авторизацию",
            variant="secondary",
            height=30,
            command=self._handle_grok_manual_submit,
        ).pack(anchor="w", padx=10, pady=(0, 6))

        self.grok_status_lbl = ctk.CTkLabel(
            self.body,
            text="Создание сессии xAI Grok...",
            font=Theme.font_body(),
            text_color=Theme.TEXT_SECONDARY,
            anchor="w",
        )
        self.grok_status_lbl.pack(fill="x", pady=4)

        self._build_step_2_footer()
        self._init_grok_oauth()

    def _on_grok_mode_toggle(self):
        self.grok_auth_mode = self.grok_mode_var.get()
        self._show_step_2_auth()

    def _init_grok_oauth(self):
        try:
            from antigravity_provider.router.grok_oauth import start_grok_oauth, get_grok_oauth_session

            session_id, url, code = start_grok_oauth(self.target_slot)
            self.grok_session_id = session_id
            self.grok_url = url
            self.grok_user_code = code

            session = get_grok_oauth_session(session_id)
            if not code or (session and session.status == "failed"):
                err_msg = getattr(session, "error_msg", None) or "Не удалось получить код авторизации"
                self.grok_status_lbl.configure(text=f"❌ {err_msg}", text_color=Theme.STATUS_ERROR)
                return

            self.grok_url_entry.delete(0, "end")
            self.grok_url_entry.insert(0, url)
            self.grok_code_lbl.configure(text=code)

            if getattr(session, "is_dev_mode", False):
                self.grok_status_lbl.configure(
                    text=f"⚠️ ТЕСТОВЫЙ РЕЖИМ (HERMES_HUB_DEV_MODE): Код {code}",
                    text_color=Theme.STATUS_WARNING,
                )
            else:
                self.grok_status_lbl.configure(
                    text=f"Ожидание подтверждения кода {code} в браузере...",
                    text_color=Theme.TEXT_SECONDARY,
                )

            self._polling_active = True
            threading.Thread(target=self._poll_grok_oauth, daemon=True).start()
        except Exception as e:
            self.grok_status_lbl.configure(text=f"Ошибка: {e}", text_color=Theme.STATUS_ERROR)

    def _copy_grok_url(self):
        if self.grok_url:
            self.clipboard_clear()
            self.clipboard_append(self.grok_url)

    def _copy_grok_code(self):
        if self.grok_user_code:
            self.clipboard_clear()
            self.clipboard_append(self.grok_user_code)

    def _open_grok_browser(self):
        if self.grok_url:
            self._copy_grok_code()
            self.grok_status_lbl.configure(
                text=(
                    "Код скопирован. Вставьте его на открывшейся странице xAI; "
                    "затем вернитесь сюда — Hub продолжит автоматически."
                ),
                text_color=Theme.TEXT_SECONDARY,
            )
            webbrowser.open(self.grok_url)

    def _handle_grok_manual_submit(self):
        raw = self.grok_manual_entry.get().strip()
        if not raw:
            self.grok_status_lbl.configure(text="❌ Введите данные токена", text_color=Theme.STATUS_ERROR)
            return

        from antigravity_provider.router.grok_oauth import get_grok_oauth_session

        session = get_grok_oauth_session(self.grok_session_id)
        if not session:
            self.grok_status_lbl.configure(text="❌ Сессия не найдена", text_color=Theme.STATUS_ERROR)
            return

        ok, msg = session.handle_manual_input(raw)
        if ok:
            info = getattr(session, "completed_profile_info", {}) or {}
            self.discovered_identity = info.get("email") or "Grok Account"
            self.discovered_plan = "Grok Pro"
            self.discovered_models = ["grok-3", "grok-3-mini", "grok-2"]
            self.is_verified = True
            self._show_step_3_validation()
        else:
            self.grok_status_lbl.configure(text=f"❌ {msg}", text_color=Theme.STATUS_ERROR)

    def _poll_grok_oauth(self):
        from antigravity_provider.router.grok_oauth import get_grok_oauth_session

        for _ in range(900):
            if not self._polling_active:
                return
            time.sleep(1)
            session = get_grok_oauth_session(self.grok_session_id)
            if not session:
                continue

            status = getattr(session, "status", "").lower()
            if status in ("completed", "success"):
                info = getattr(session, "completed_profile_info", {}) or {}
                self.discovered_identity = info.get("email") or "Grok Account"
                self.discovered_plan = "Grok Pro"
                self.discovered_models = ["grok-3", "grok-3-mini", "grok-2"]
                self.is_verified = True
                self.after(0, self._show_step_3_validation)
                return

    # ─────────────────────────────────────────────────────────────
    #  API KEY FLOW
    # ─────────────────────────────────────────────────────────────

    def _build_api_key_flow(self):
        # Mode switchers for Codex, Claude, Grok
        if self.selected_provider == "openai-codex":
            mode_card = HubCard(self.body, fg_color="transparent")
            mode_card.pack(fill="x", pady=(0, 8))
            self.codex_mode_var = ctk.StringVar(value="api_key")
            ctk.CTkRadioButton(
                mode_card,
                text="OAuth — OpenAI / ChatGPT",
                value="oauth",
                variable=self.codex_mode_var,
                font=Theme.font_body(),
                fg_color=Theme.ACCENT,
                command=self._on_codex_mode_toggle,
            ).pack(anchor="w", padx=4, pady=2)
            ctk.CTkRadioButton(
                mode_card,
                text="API Key — OpenAI API (sk-...)",
                value="api_key",
                variable=self.codex_mode_var,
                font=Theme.font_body_bold(),
                fg_color=Theme.ACCENT,
                command=self._on_codex_mode_toggle,
            ).pack(anchor="w", padx=4, pady=2)
        elif self.selected_provider == "claude":
            mode_card = HubCard(self.body, fg_color="transparent")
            mode_card.pack(fill="x", pady=(0, 8))
            self.claude_mode_var = ctk.StringVar(value="api_key")
            ctk.CTkRadioButton(
                mode_card,
                text="OAuth — Claude Pro/Max",
                value="oauth",
                variable=self.claude_mode_var,
                font=Theme.font_body(),
                fg_color=Theme.PROVIDER_CLAUDE,
                command=self._on_claude_mode_toggle,
            ).pack(anchor="w", padx=4, pady=2)
            ctk.CTkRadioButton(
                mode_card,
                text="API Key — Anthropic API (sk-ant-...)",
                value="api_key",
                variable=self.claude_mode_var,
                font=Theme.font_body_bold(),
                fg_color=Theme.PROVIDER_CLAUDE,
                command=self._on_claude_mode_toggle,
            ).pack(anchor="w", padx=4, pady=2)
        elif self.selected_provider == "grok":
            mode_card = HubCard(self.body, fg_color="transparent")
            mode_card.pack(fill="x", pady=(0, 8))
            self.grok_mode_var = ctk.StringVar(value="api_key")
            ctk.CTkRadioButton(
                mode_card,
                text="OAuth — xAI / SuperGrok",
                value="oauth",
                variable=self.grok_mode_var,
                font=Theme.font_body(),
                fg_color=Theme.PROVIDER_GROK,
                command=self._on_grok_mode_toggle,
            ).pack(anchor="w", padx=4, pady=2)
            ctk.CTkRadioButton(
                mode_card,
                text="API Key — xAI API (xai-...)",
                value="api_key",
                variable=self.grok_mode_var,
                font=Theme.font_body_bold(),
                fg_color=Theme.PROVIDER_GROK,
                command=self._on_grok_mode_toggle,
            ).pack(anchor="w", padx=4, pady=2)

        prompt_map = {
            "openai-codex": "Введите ключ OpenAI API (sk-...):",
            "claude": "Введите ключ Anthropic API (sk-ant-...):",
            "grok": "Введите ключ xAI API (xai-...):",
            "opencode-go": "Введите ключ API / Bearer Token для OpenCode Go:",
        }
        ctk.CTkLabel(
            self.body,
            text=prompt_map.get(self.selected_provider, "Введите ключ API:"),
            font=Theme.font_body(),
            text_color=Theme.TEXT_SECONDARY,
        ).pack(anchor="w", pady=(0, 6))

        entry_row = ctk.CTkFrame(self.body, fg_color="transparent")
        entry_row.pack(fill="x", pady=(0, 8))

        placeholder_map = {
            "openai-codex": "sk-...",
            "claude": "sk-ant-...",
            "grok": "xai-...",
            "opencode-go": "opencode-...",
        }
        self.key_entry = HubEntry(
            entry_row,
            placeholder_text=placeholder_map.get(self.selected_provider, "sk-..."),
            font=Theme.font_mono(),
            show="" if self.selected_provider == "opencode-go" else "*",
            height=38,
            fg_color=Theme.PRIMARY,
            border_color=Theme.BORDER,
            text_color=Theme.TEXT_PRIMARY,
        )
        self.key_entry.pack(side="left", fill="x", expand=True, padx=(0, 8))
        self.key_entry.focus_set()

        HubButton(
            entry_row,
            text="📋 Вставить",
            variant="secondary",
            width=90,
            height=38,
            command=lambda: self._paste_into_entry(self.key_entry),
        ).pack(side="right")

        self.key_status_lbl = ctk.CTkLabel(
            self.body,
            text="",
            font=Theme.font_caption(),
            text_color=Theme.STATUS_ERROR,
        )
        self.key_status_lbl.pack(anchor="w")

        def _save_key():
            k = self.key_entry.get().strip()
            if not k:
                self.key_status_lbl.configure(text="Пожалуйста, введите ключ API.")
                return

            is_valid = False
            masked_id = None
            models: List[str] = []

            if self.selected_provider == "openai-codex":
                is_valid, masked_id, models = ProfileAuthManager.verify_codex_token(k)
                self.discovered_plan = "PLUS"
            elif self.selected_provider == "claude":
                is_valid, masked_id, models = ProfileAuthManager.verify_claude_token(k)
                self.discovered_plan = "MAX"
            elif self.selected_provider == "grok":
                is_valid, masked_id, models = ProfileAuthManager.verify_grok_token(k)
                self.discovered_plan = "Grok Pro"
            elif self.selected_provider == "opencode-go":
                is_valid, masked_id, models = ProfileAuthManager.verify_opencode_token(k)
                self.discovered_plan = "SUBSCRIPTION"

            auth_data = {
                "provider": self.selected_provider,
                "profile_id": self.target_slot,
                "auth_mode": "api_key",
                "api_key": k,
                "created_at": time.time(),
            }
            ProfileAuthManager.save_profile_auth(self.selected_provider, self.target_slot, auth_data)

            self.is_verified = is_valid
            self.discovered_identity = masked_id or (k[:8] + "..." + k[-4:] if len(k) > 12 else "API Key")
            self.discovered_models = models if is_valid else []
            self._show_step_3_validation()

        self._build_step_2_footer(next_cmd=_save_key)

    def _build_step_2_footer(self, next_cmd: Optional[Callable] = None):
        HubButton(
            self.footer,
            text="← Назад",
            variant="ghost",
            command=self._show_step_1_provider,
        ).pack(side="left", padx=10, pady=10)

        if next_cmd:
            HubButton(
                self.footer,
                text="Проверить и продолжить →",
                variant="primary",
                command=next_cmd,
            ).pack(side="right", padx=10, pady=10)

    def _paste_into_entry(self, entry_widget: HubEntry):
        try:
            content = entry_widget.clipboard_get().strip()
            if content:
                entry_widget.delete(0, "end")
                entry_widget.insert(0, content)
                entry_widget.focus_set()
                entry_widget.icursor("end")
                if hasattr(self, "key_status_lbl") and entry_widget is getattr(self, "key_entry", None):
                    self.key_status_lbl.configure(text="✓ Ключ вставлен. Нажмите «Проверить и продолжить».")
            elif hasattr(self, "key_status_lbl"):
                self.key_status_lbl.configure(text="Буфер обмена пуст.")
        except Exception as exc:
            if hasattr(self, "key_status_lbl"):
                self.key_status_lbl.configure(text=f"Не удалось вставить: {exc}")

    # ═══════════════════════════════════════════════════════════════
    #  STEP 3: Validation & Identity
    # ═══════════════════════════════════════════════════════════════

    def _show_step_3_validation(self):
        self._polling_active = False
        self._clear_body()
        self.step = 3
        self.title_lbl.configure(text="Шаг 3 из 4: Проверка учетной записи")

        # Duplicate check
        dup_profile = AutoAssigner.check_duplicate_identity(
            self.selected_provider,
            self.discovered_identity,
            exclude_profile_id=self.target_slot,
        )

        card = HubCard(self.body)
        card.pack(fill="x", pady=10)

        ctk.CTkLabel(
            card,
            text="Результат проверки подключения:",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
        ).pack(anchor="w", padx=14, pady=(10, 6))

        # Identity line
        id_row = ctk.CTkFrame(card, fg_color="transparent")
        id_row.pack(fill="x", padx=14, pady=2)
        ctk.CTkLabel(id_row, text="Идентификатор:", font=Theme.font_caption(), text_color=Theme.TEXT_SECONDARY).pack(
            side="left"
        )
        ctk.CTkLabel(
            id_row, text=self.discovered_identity or "—", font=Theme.font_body_bold(), text_color=Theme.TEXT_PRIMARY
        ).pack(side="right")

        # Plan line
        plan_row = ctk.CTkFrame(card, fg_color="transparent")
        plan_row.pack(fill="x", padx=14, pady=2)
        ctk.CTkLabel(plan_row, text="Тариф:", font=Theme.font_caption(), text_color=Theme.TEXT_SECONDARY).pack(
            side="left"
        )
        ctk.CTkLabel(
            plan_row,
            text=self.discovered_plan or "Тариф: неизвестен",
            font=Theme.font_body_bold(),
            text_color=Theme.ACCENT,
        ).pack(side="right")

        # Status line
        stat_row = ctk.CTkFrame(card, fg_color="transparent")
        stat_row.pack(fill="x", padx=14, pady=2)
        ctk.CTkLabel(
            stat_row, text="Статус проверки:", font=Theme.font_caption(), text_color=Theme.TEXT_SECONDARY
        ).pack(side="left")
        stat_lbl = "✓ Успешно авторизован" if self.is_verified else "Подключен (без проверки)"
        stat_col = Theme.STATUS_HEALTHY if self.is_verified else Theme.STATUS_WARNING
        ctk.CTkLabel(stat_row, text=stat_lbl, font=Theme.font_body_bold(), text_color=stat_col).pack(side="right")

        if dup_profile:
            warn_card = HubCard(self.body, border_color=Theme.STATUS_WARNING, fg_color=Theme.ACCENT_DIM)
            warn_card.pack(fill="x", pady=6)
            ctk.CTkLabel(
                warn_card,
                text=f"⚠️ Этот аккаунт уже подключен к слоту {dup_profile}.",
                font=Theme.font_caption(),
                text_color=Theme.STATUS_WARNING,
            ).pack(padx=10, pady=8)

        HubButton(
            self.footer,
            text="← Назад",
            variant="ghost",
            command=self._show_step_2_auth,
        ).pack(side="left", padx=10, pady=10)

        HubButton(
            self.footer,
            text="Продолжить к распределению роли →",
            variant="primary",
            command=self._show_step_4_assignment,
        ).pack(side="right", padx=10, pady=10)

    # ═══════════════════════════════════════════════════════════════
    #  STEP 4: Auto-Assignment & Completion
    # ═══════════════════════════════════════════════════════════════

    def _show_step_4_assignment(self):
        self._clear_body()
        self.step = 4
        self.title_lbl.configure(text="Шаг 4 из 4: Назначение роли в роутере")

        disp_name, role_code, tier = AutoAssigner.get_display_name_and_role(self.target_slot)

        card = HubCard(self.body)
        card.pack(fill="x", pady=10)

        ctk.CTkLabel(
            card,
            text="Назначенная роль в команде:",
            font=Theme.font_body_bold(),
            text_color=Theme.TEXT_PRIMARY,
        ).pack(anchor="w", padx=14, pady=(10, 6))

        r_row = ctk.CTkFrame(card, fg_color="transparent")
        r_row.pack(fill="x", padx=14, pady=4)
        ctk.CTkLabel(r_row, text="Роль агента:", font=Theme.font_caption(), text_color=Theme.TEXT_SECONDARY).pack(
            side="left"
        )
        ctk.CTkLabel(r_row, text=disp_name, font=Theme.font_heading(), text_color=Theme.TEXT_PRIMARY).pack(side="right")

        s_row = ctk.CTkFrame(card, fg_color="transparent")
        s_row.pack(fill="x", padx=14, pady=4)
        ctk.CTkLabel(s_row, text="Внутренний слот:", font=Theme.font_caption(), text_color=Theme.TEXT_SECONDARY).pack(
            side="left"
        )
        ctk.CTkLabel(s_row, text=self.target_slot, font=Theme.font_mono(), text_color=Theme.TEXT_MUTED).pack(
            side="right"
        )

        ctk.CTkLabel(
            self.body,
            text="Аккаунт готов к работе в роутере Hermes Hub и приему запросов.",
            font=Theme.font_caption(),
            text_color=Theme.TEXT_SECONDARY,
            anchor="w",
        ).pack(fill="x", pady=8)

        self.finish_status_lbl = ctk.CTkLabel(
            self.body,
            text="Нажмите «Завершить подключение», чтобы сохранить роль и обновить Hub.",
            font=Theme.font_caption(),
            text_color=Theme.TEXT_SECONDARY,
            anchor="w",
        )
        self.finish_status_lbl.pack(fill="x", pady=(0, 4))

        HubButton(
            self.footer,
            text="✓ Завершить подключение",
            variant="primary",
            command=self._finish,
        ).pack(side="right", padx=10, pady=10)

    def _finish(self):
        if not self.target_slot:
            self.finish_status_lbl.configure(
                text="❌ Подключение невозможно: свободный слот не найден.",
                text_color=Theme.STATUS_ERROR,
            )
            return
        profile_ok, profile_message = AutoAssigner.ensure_profile_definition(
            self.selected_provider,
            self.target_slot,
        )
        if not profile_ok:
            self.finish_status_lbl.configure(text=f"❌ {profile_message}", text_color=Theme.STATUS_ERROR)
            return
        # Most built-in slots already occur in a default chain.  Custom or
        # repaired configs may not, so completion makes that invariant explicit
        # without reordering a slot that is already assigned.
        ok, message = ensure_profile_in_routing(self.target_slot)
        if not ok:
            self.finish_status_lbl.configure(text=f"❌ {message}", text_color=Theme.STATUS_ERROR)
            return

        # Всё, что проверяемо, уже проверено выше и сообщает об ошибке в самом
        # окне. Дальше идут побочные действия, и ни одно из них не должно
        # мешать закрытию: исключение здесь оставляло мастер открытым без
        # единого признака ошибки, потому что под pythonw трейсбека не видно.
        try:
            # A reused slot can carry cooldown from an older account.  Fresh OAuth
            # credentials must start with fresh health state.
            from antigravity_provider.router.router_engine import get_router_engine

            get_router_engine().health.clear_cooldown(self.target_slot)
            EventLogService.get().log(
                "account",
                f"Подключен аккаунт {self.selected_provider}; слот {self.target_slot}; роль сохранена.",
                level="success",
            )
        except Exception:
            pass

        if self.on_complete:
            try:
                self.on_complete(
                    {
                        "provider": self.selected_provider,
                        "profile_id": self.target_slot,
                        "identity": self.discovered_identity,
                    }
                )
            except Exception:
                pass

        self.destroy()
