# -*- coding: utf-8 -*-
"""CustomTkinter main shell + settings window for Claude Proxy Control."""
from __future__ import annotations

import tkinter as tk
from tkinter import ttk
from typing import Any, Callable

import customtkinter as ctk

from ui_theme import BUTTONS as B
from ui_theme import COLORS as C
from ui_theme import FONTS as F
from ui_theme import label as btn_label
from version import version_line


def button(
    parent: Any,
    text: str,
    command: Callable[[], None] | None = None,
    *,
    style: str = "soft",
    icon: str | None = None,
    **kwargs: Any,
) -> ctk.CTkButton:
    """Кнопка одного из стилей из ui_theme.

    Раньше каждая кнопка красилась вручную прямо в месте создания, и половина
    из них получила `surface2` — заливку, которая от фона окна отличается на
    1.05: кнопки на экране просто не было видно. Теперь цвет берётся из одного
    места, а рамка есть всегда — на кремовом фоне светлая заливка без рамки
    теряет края.
    """
    s = B.get(style) or B["soft"]
    kwargs.setdefault("font", F["button_sm"])
    kwargs.setdefault("corner_radius", 10)
    kwargs.setdefault("border_width", 1)
    return ctk.CTkButton(
        parent,
        text=btn_label(icon, text),
        command=command,
        fg_color=s["fg"],
        hover_color=s["hover"],
        text_color=s["text"],
        # У CustomTkinter свой серый для выключенной кнопки (gray60), и он
        # ложится поверх нашего: на «Включаю…» текст выцветал до нечитаемого.
        # Цвет для выключенного состояния задаём сами — он уже подобран.
        text_color_disabled=s["text"],
        border_color=s["border"],
        **kwargs,
    )


def apply_ctk_globals() -> None:
    ctk.set_appearance_mode("light")
    ctk.set_default_color_theme("blue")


def _card(parent: Any, **kwargs: Any) -> ctk.CTkFrame:
    return ctk.CTkFrame(
        parent,
        fg_color=C["surface"],
        border_width=1,
        border_color=C["border"],
        corner_radius=14,
        **kwargs,
    )


def _link(parent: Any, text: str, command: Callable[[], None]) -> ctk.CTkLabel:
    """Ссылка, а не кнопка: внизу экрана нужен спокойный ряд, а не три кнопки.

    Подчёркивание и смена цвета под курсором — единственный способ показать, что
    это можно нажать: у CTkLabel нет своего состояния наведения.
    """
    lbl = ctk.CTkLabel(
        parent,
        text=text,
        font=F["small"],
        text_color=C["info_fg"],
        cursor="hand2",
    )
    lbl.bind("<Button-1>", lambda _e: command())
    lbl.bind("<Enter>", lambda _e: lbl.configure(text_color=C["accent_hover"]))
    lbl.bind("<Leave>", lambda _e: lbl.configure(text_color=C["info_fg"]))
    return lbl


def build_main(app: Any) -> None:
    """Compact home: status cards on top, action buttons pinned at bottom."""
    apply_ctk_globals()
    app.configure(fg_color=C["bg"])
    # Окно стало ниже: с главного экрана ушли две карточки диагностики, и на
    # прежней высоте между содержимым и кнопками зияла пустота.
    app.geometry("460x560")
    app.minsize(430, 520)

    shell = ctk.CTkFrame(app, fg_color=C["bg"])
    shell.pack(fill="both", expand=True, padx=14, pady=12)

    # ── Footer first (pack side=bottom) so Start/Stop always visible ──
    footer = ctk.CTkFrame(shell, fg_color="transparent")
    footer.pack(side="bottom", fill="x", pady=(8, 0))

    # Две кнопки, а не одна на всё: туннель и Claude — независимые состояния.
    # Пока кнопка была общей, на машине без Claude нельзя было даже поднять
    # маршрутизацию, а надпись «Запустить Claude» ничего не говорила о туннеле.
    app.btn_tun = button(
        footer,
        "Включить защиту",
        app.on_tun_button,
        style="primary",
        icon="start",
        height=40,
        font=F["button"],
    )
    app.btn_tun.pack(fill="x", pady=(0, 6))

    app.btn_primary = button(
        footer,
        "Открыть Claude",
        app.on_start_claude,
        style="primary",
        icon="start",
        height=40,
        font=F["button"],
    )
    app.btn_primary.pack(fill="x", pady=(0, 6))

    app.btn_stop_claude = button(
        footer,
        "Выключить защиту",
        app.on_stop_claude,
        style="danger",
        icon="stop",
        height=34,
    )
    app.btn_stop_claude.pack(fill="x", pady=(0, 6))

    app.lbl_primary_hint = ctk.CTkLabel(
        footer,
        text="",
        font=F["small"],
        text_color=C["muted"],
        anchor="w",
        wraplength=420,
        justify="left",
    )
    app.lbl_primary_hint.pack(fill="x", pady=(2, 0))

    app.lbl_action_flash = ctk.CTkLabel(
        footer,
        text="",
        font=F["small"],
        text_color=C["muted"],
        anchor="w",
        wraplength=420,
        justify="left",
        height=16,
    )
    app.lbl_action_flash.pack(fill="x", pady=(0, 6))

    # Ряд ссылок вместо кнопок: продлить, поддержка, сайт. Раньше отсюда никуда
    # нельзя было попасть — за продлением человек шёл искать письмо с ключом.
    links = ctk.CTkFrame(footer, fg_color="transparent")
    links.pack(fill="x", pady=(2, 4))
    _link(links, "Продлить подписку", app.on_extend_license).pack(side="left")
    ctk.CTkLabel(links, text="·", font=F["small"], text_color=C["muted"]).pack(
        side="left", padx=6
    )
    _link(links, "Поддержка", app.on_open_support).pack(side="left")
    ctk.CTkLabel(links, text="·", font=F["small"], text_color=C["muted"]).pack(
        side="left", padx=6
    )
    _link(links, "Сайт", app.on_open_site).pack(side="left")

    # Перезапуск и закрытие — служебные действия, им место в конце и потише.
    row = ctk.CTkFrame(footer, fg_color="transparent")
    row.pack(fill="x", pady=(2, 0))
    app.btn_restart = button(
        row,
        "Перезапустить программу",
        app.on_restart_program,
        style="quiet",
        icon="restart",
        height=30,
        font=F["small"],
    )
    app.btn_restart.pack(side="left", fill="x", expand=True, padx=(0, 5))
    app.btn_quit = button(
        row,
        "Закрыть",
        app.on_quit_program,
        style="quiet",
        icon="close",
        height=30,
        font=F["small"],
    )
    app.btn_quit.pack(side="left", fill="x", expand=True, padx=(5, 0))

    # Версия — мелко и внизу, но на виду. Без неё нельзя ответить на простой
    # вопрос «у вас новая программа или старая?»: приходилось разбирать снимки
    # экрана по цвету кнопок.
    app.lbl_version = ctk.CTkLabel(
        footer,
        text=version_line(),
        font=F["small"],
        text_color=C["muted"],
        anchor="e",
    )
    app.lbl_version.pack(fill="x", pady=(6, 0))

    # ── Top content (fills remaining space) ──
    root = ctk.CTkFrame(shell, fg_color=C["bg"])
    root.pack(side="top", fill="both", expand=True)

    head = ctk.CTkFrame(root, fg_color="transparent")
    head.pack(fill="x")
    ctk.CTkLabel(
        head,
        text="Claude Proxy Control",
        font=F["brand"],
        text_color=C["text"],
        anchor="w",
    ).pack(side="left", fill="x", expand=True)
    app.btn_settings = button(
        head,
        "Настройки",
        app.open_settings,
        style="soft",
        icon="settings",
        width=118,
        height=30,
        corner_radius=8,
    )
    app.btn_settings.pack(side="right")

    ctk.CTkLabel(
        root,
        text="Ваш доступ к Claude",
        font=F["small"],
        text_color=C["muted"],
        anchor="w",
    ).pack(fill="x", pady=(2, 10))

    # ── Одна карточка состояния вместо трёх ──────────────────────────────────
    # Раньше здесь были «Лицензия», «Сеть» и «Статус» — три блока, в которых
    # человек видел Squid, TUN, узлы и проценты. Ему нужно знать одно: работает
    # или нет, и что делать. Подробности живут в настройках и в отчёте.
    state = _card(root)
    state.pack(fill="x", pady=(0, 8))
    app.prog_box = state
    app.status_box = state
    app.status_accent = state
    app._status_body = state
    pin = ctk.CTkFrame(state, fg_color="transparent")
    pin.pack(fill="x", padx=16, pady=14)

    app.lbl_mon_overall = ctk.CTkLabel(
        pin,
        text="Проверяю…",
        font=F["state"],
        text_color=C["text"],
        anchor="w",
        wraplength=390,
        justify="left",
    )
    app.lbl_mon_overall.pack(fill="x")

    app.lbl_prog_title = ctk.CTkLabel(
        pin,
        text="",
        font=F["body"],
        text_color=C["muted"],
        anchor="w",
        wraplength=390,
        justify="left",
    )
    app.lbl_prog_title.pack(fill="x", pady=(4, 0))
    app.lbl_status = app.lbl_prog_title

    app.lbl_prog_step = ctk.CTkLabel(
        pin,
        text="",
        font=F["small"],
        text_color=C["muted"],
        anchor="w",
        wraplength=390,
        justify="left",
    )
    app.lbl_prog_step.pack(fill="x", pady=(2, 0))
    app.lbl_next = app.lbl_prog_step

    # Предупреждение показываем только когда оно есть — пустая жёлтая строка
    # на светлом фоне читается как «что-то не так», даже если всё в порядке.
    app.lbl_prog_warn = ctk.CTkLabel(
        pin,
        text="",
        font=F["small"],
        text_color=C["warn_fg"],
        anchor="w",
        wraplength=390,
        justify="left",
    )
    app.btn_copy_prog = button(
        pin,
        "Скопировать текст ошибки",
        app.on_copy_start_progress,
        style="quiet",
        icon="copy",
        height=26,
        font=F["small"],
        state="disabled",
    )
    app._start_progress_copy = ""
    app._start_progress_polling = False

    # ── Подписка ─────────────────────────────────────────────────────────────
    lic = _card(root)
    lic.pack(fill="x", pady=(0, 8))
    lic_inner = ctk.CTkFrame(lic, fg_color="transparent")
    lic_inner.pack(fill="x", padx=16, pady=12)
    app.lbl_license = ctk.CTkLabel(
        lic_inner,
        text="Подписка: проверяю…",
        font=F["title"],
        text_color=C["text"],
        anchor="w",
        justify="left",
        wraplength=390,
    )
    app.lbl_license.pack(fill="x")
    app.lbl_runtime_summary = ctk.CTkLabel(
        lic_inner,
        text="",
        font=F["small"],
        text_color=C["muted"],
        anchor="w",
        justify="left",
        wraplength=390,
    )
    app.lbl_runtime_summary.pack(fill="x", pady=(3, 0))
    app.lbl_claude_install = app.lbl_runtime_summary

    # Скрытые ярлыки: код в других местах их трогает, но на экране они лишние.
    hidden = ctk.CTkFrame(root, fg_color="transparent")
    app.lbl_mon_detail = ctk.CTkLabel(hidden, text="")
    app.lbl_prog_head = ctk.CTkLabel(hidden, text="")
    app.btn_extend = ctk.CTkButton(hidden, text="Продлить", width=1, height=1)
    app.btn_change_key = ctk.CTkButton(hidden, text="Ключ", width=1, height=1)
    for alias in (
        "lbl_mon_squid",
        "lbl_mon_vpn",
        "lbl_mon_eg",
        "lbl_mon_note",
        "lbl_active",
        "lbl_s1",
        "lbl_s2",
        "lbl_s3",
    ):
        setattr(app, alias, app.lbl_mon_detail)

    app._action_cards = {}
    app._nb = None
    app.live = None
    app.tree = None
    app.direct_tree = None
    app._settings_win = None
    app._nodes_geo_busy = False
    app.var_apps_filter = tk.StringVar(value="")
    app._apps_list_cache = []


class SettingsWindow(ctk.CTkToplevel):
    """Secondary window: apps, nodes, network tools, account."""

    def __init__(self, app: Any) -> None:
        super().__init__(app)
        self.app = app
        self.title("Настройки — Claude Proxy Control")
        self.geometry("920x680")
        self.minsize(780, 560)
        self.configure(fg_color=C["bg"])
        self.protocol("WM_DELETE_WINDOW", self._on_close)

        tabs = ctk.CTkTabview(
            self,
            fg_color=C["surface"],
            segmented_button_fg_color=C["surface2"],
            segmented_button_selected_color=C["accent"],
            segmented_button_selected_hover_color=C["accent_hover"],
            segmented_button_unselected_color=C["surface2"],
            segmented_button_unselected_hover_color=C["border"],
            text_color=C["text"],
            corner_radius=12,
        )
        tabs.pack(fill="both", expand=True, padx=16, pady=16)
        self.tabs = tabs

        tab_apps = tabs.add("Приложения")
        tab_nodes = tabs.add("Узлы")
        tab_net = tabs.add("Сеть")
        tab_acc = tabs.add("Аккаунт")
        tab_adv = tabs.add("Сервис")

        self._build_apps(tab_apps)
        self._build_nodes(tab_nodes)
        self._build_net(tab_net)
        self._build_account(tab_acc)
        self._build_adv(tab_adv)

        app.live = self.live
        app.tree = self.tree
        app.direct_tree = self.live
        app._nb = tabs
        app.ent_apps_filter = self.ent_apps_filter
        app.lbl_apps_filter_count = self.lbl_apps_filter_count
        app.var_apps_filter = self.var_apps_filter

        app._bind_apps_context_menu(self.live)
        app._bind_nodes_context_menu(self.tree)
        app._refresh_apps_async()
        app._refresh_nodes_tree()
        # Состояние ловца — сразу, а не на следующем тике: иначе окно на
        # секунду показывает «Проверяю…» и кнопки в исходном виде.
        app._paint_watcher_block()

    def _on_close(self) -> None:
        # Виджеты сейчас будут уничтожены — обнуляем ссылки, иначе фоновая
        # отрисовка списка полезет в мёртвый Treeview и словит TclError.
        self.app._settings_win = None
        self.app.live = None
        self.app.direct_tree = None
        self.app.tree = None
        # Виджеты вкладки «Сервис» тоже умирают вместе с окном: фоновая
        # перерисовка состояния ловца иначе полезет в уничтоженный ярлык.
        self.app.lbl_watcher = None
        self.app.btn_watcher_install = None
        self.app.btn_watcher_stop = None
        self.destroy()

    def _style_tree(self, tree: ttk.Treeview) -> None:
        style = ttk.Style(tree)
        try:
            style.theme_use("clam")
        except Exception:
            pass
        style.configure(
            "Settings.Treeview",
            background=C["surface2"],
            fieldbackground=C["surface2"],
            foreground=C["text"],
            rowheight=26,
            font=("Segoe UI", 10),
            borderwidth=0,
        )
        style.configure(
            "Settings.Treeview.Heading",
            background=C["surface"],
            foreground=C["muted"],
            font=("Segoe UI Semibold", 10),
        )
        tree.configure(style="Settings.Treeview")

    def _make_tree(
        self,
        parent: Any,
        columns: tuple[str, ...],
        headings: tuple[tuple[str, str, int], ...],
        height: int = 12,
    ) -> ttk.Treeview:
        wrap = tk.Frame(parent, bg=C["surface"])
        wrap.pack(fill="both", expand=True, pady=(6, 0))
        vsb = ttk.Scrollbar(wrap, orient="vertical")
        tree = ttk.Treeview(
            wrap,
            columns=columns,
            show="headings",
            height=height,
            yscrollcommand=vsb.set,
        )
        vsb.configure(command=tree.yview)
        vsb.pack(side="right", fill="y")
        tree.pack(side="left", fill="both", expand=True)
        for col, title, width in headings:
            tree.heading(col, text=title)
            tree.column(col, width=width, minwidth=40)
        self._style_tree(tree)
        return tree

    def _build_apps(self, tab: Any) -> None:
        ctk.CTkLabel(
            tab,
            text=(
                "Кружки: 🟢 зелёный — программа недавно ходила в интернет; "
                "⚪ серый — сейчас тихо, активности нет.\n"
                "Смена канала: сразу блокирует старый путь и сама применяет маршруты "
                "(закроет Claude → новый TUN → откроет Claude). Нужен UAC. "
                "В колонке «Канал» жёлтым видно, если ещё ждём перезапуск TUN. "
                "Без правила у новой программы выхода нет. Claude всегда через Squid."
            ),
            font=F["small"],
            text_color=C["muted"],
            anchor="w",
            justify="left",
            wraplength=880,
        ).pack(fill="x", pady=(8, 6))
        fr = ctk.CTkFrame(tab, fg_color="transparent")
        fr.pack(fill="x")
        ctk.CTkLabel(fr, text="Фильтр", text_color=C["muted"], font=F["small"]).pack(
            side="left", padx=(0, 8)
        )
        self.var_apps_filter = tk.StringVar(value="")
        self.ent_apps_filter = ctk.CTkEntry(fr, textvariable=self.var_apps_filter, width=320)
        self.ent_apps_filter.pack(side="left")
        self.lbl_apps_filter_count = ctk.CTkLabel(fr, text="", text_color=C["muted"], font=F["small"])
        self.lbl_apps_filter_count.pack(side="left", padx=10)
        button(
            fr, "✕", self.app._clear_apps_filter, style="quiet", width=36
        ).pack(side="left")
        self.var_apps_filter.trace_add("write", lambda *_: self.app._on_apps_filter_changed())

        self.live = self._make_tree(
            tab,
            columns=("act", "channel", "exe", "path", "seen"),
            headings=(
                ("act", "", 40),
                ("channel", "Канал", 320),
                ("exe", "Программа", 130),
                ("path", "Путь", 280),
                ("seen", "Последний раз", 130),
            ),
            height=14,
        )
        # Цвета строк остались от тёмной темы: мятный #7ddea0 и золотой #ffd666
        # на бежевом фоне списка выцветали до нечитаемого. Те же смыслы, но в
        # тёмных тонах — из общей палитры.
        for tag, color in (
            ("direct", C["ok_fg"]),
            ("vpn", C["info_fg"]),
            ("proxy", C["ok_fg"]),
            ("block", C["bad_fg"]),
            ("pending", C["warn_fg"]),
            ("claude", C["ok_fg"]),
            ("stale", C["warn_fg"]),
        ):
            self.live.tag_configure(tag, foreground=color)

        actions = ctk.CTkFrame(tab, fg_color="transparent")
        actions.pack(fill="x", pady=(10, 0))
        button(
            actions,
            "Добавить exe…",
            self.app.on_bypass_browse,
            style="soft",
            width=160,
            height=36,
        ).pack(side="left", padx=(0, 8))
        self.btn_apply_routes = button(
            actions,
            "Применить маршруты",
            self.app.on_apply_routes,
            style="primary",
            icon="apply",
            width=200,
            height=36,
        )
        self.btn_apply_routes.pack(side="left", padx=(0, 10))
        self.lbl_routes_status = ctk.CTkLabel(
            actions,
            text="Сменил канал → нажми «Применить маршруты» (будет запрос UAC)",
            font=F["small"],
            text_color=C["muted"],
            anchor="w",
            justify="left",
            wraplength=480,
        )
        self.lbl_routes_status.pack(side="left", fill="x", expand=True)

        self.app.btn_apply_routes = self.btn_apply_routes
        self.app.lbl_routes_status = self.lbl_routes_status

    def _build_nodes(self, tab: Any) -> None:
        ctk.CTkLabel(
            tab,
            text="VPN-узлы из Sota. Активный ★ · ПКМ или двойной клик — сделать активным.",
            font=F["small"],
            text_color=C["muted"],
            anchor="w",
            wraplength=840,
        ).pack(fill="x", pady=(8, 6))
        self.tree = self._make_tree(
            tab,
            columns=("vpn", "country", "server", "when"),
            headings=(
                ("vpn", "VPN", 70),
                ("country", "Страна", 150),
                ("server", "Сервер", 240),
                ("when", "Добавлен", 180),
            ),
            height=14,
        )
        self.tree.tag_configure("active", foreground=C["ok_fg"])
        self.tree.tag_configure("idle", foreground=C["muted"])
        row = ctk.CTkFrame(tab, fg_color="transparent")
        row.pack(fill="x", pady=(10, 0))
        button(
            row,
            "Обновить узел (Sota)",
            self.app.on_help_capture,
            style="info",
            icon="apply",
            height=36,
        ).pack(side="left")

    def _build_net(self, tab: Any) -> None:
        ctk.CTkLabel(
            tab,
            text="Проверки и инструменты сети",
            font=F["title"],
            text_color=C["text"],
            anchor="w",
        ).pack(fill="x", pady=(8, 10))
        grid = ctk.CTkFrame(tab, fg_color="transparent")
        grid.pack(fill="x")
        buttons: list[tuple[str, Callable[[], None]]] = [
            ("Проверить сейчас", self.app.on_check_network),
            ("Обновить статус", self.app.on_refresh_status),
            ("Готовность TUN", self.app.on_tun_ready),
            ("Авто-установка", self.app.on_bootstrap_services),
            ("Диагностика → отчёт", self.app.on_support_report),
            ("Проверить обновления", self.app.on_check_updates),
        ]
        for i, (text, cmd) in enumerate(buttons):
            button(grid, text, cmd, style="soft", height=40).grid(
                row=i // 2, column=i % 2, sticky="ew", padx=6, pady=6
            )
        grid.columnconfigure(0, weight=1)
        grid.columnconfigure(1, weight=1)

    def _build_account(self, tab: Any) -> None:
        ctk.CTkLabel(
            tab,
            text="Подписка и ключ",
            font=F["title"],
            text_color=C["text"],
            anchor="w",
        ).pack(fill="x", pady=(8, 10))
        button(
            tab,
            "Продлить лицензию",
            self.app.on_extend_license,
            style="primary",
            height=42,
            font=F["button"],
        ).pack(fill="x", pady=6)
        button(
            tab,
            "Сменить ключ",
            self.app.on_change_key,
            style="info",
            height=42,
            font=F["button"],
        ).pack(fill="x", pady=6)
        ctk.CTkLabel(
            tab,
            text="Кабинет: https://proxy.mindq.ru · API: https://api.proxy.mindq.ru",
            font=F["small"],
            text_color=C["muted"],
            anchor="w",
        ).pack(fill="x", pady=(16, 0))

    def _build_adv(self, tab: Any) -> None:
        # Кто включает защиту — человек или программа. По умолчанию человек:
        # окно открывают и просто чтобы посмотреть, а поднятый туннель
        # перестраивает сеть и просит разрешение у Windows.
        ctk.CTkLabel(
            tab,
            text="Запуск",
            font=F["title"],
            text_color=C["text"],
            anchor="w",
        ).pack(fill="x", pady=(8, 6))
        self.var_autostart = tk.BooleanVar(value=bool(self.app.autostart_enabled()))
        ctk.CTkCheckBox(
            tab,
            text="Включать защиту сразу при запуске программы",
            variable=self.var_autostart,
            command=lambda: self.app.on_toggle_autostart(bool(self.var_autostart.get())),
            font=F["body"],
            text_color=C["text"],
            fg_color=B["primary"]["fg"],
            hover_color=B["primary"]["hover"],
            border_color=C["border"],
            checkmark_color="#ffffff",
        ).pack(anchor="w", pady=(0, 4))
        ctk.CTkLabel(
            tab,
            text="Выключено — защиту включаете вы кнопкой на главном экране. "
            "Включено — программа поднимет её сама и попросит разрешение Windows.",
            font=F["small"],
            text_color=C["muted"],
            anchor="w",
            justify="left",
            wraplength=560,
        ).pack(fill="x", pady=(0, 12))

        ctk.CTkLabel(
            tab,
            text="Сервисные действия",
            font=F["title"],
            text_color=C["text"],
            anchor="w",
        ).pack(fill="x", pady=(8, 10))
        # Ловец узлов: сначала его состояние, потом кнопки — и кнопка доступна
        # только тогда, когда её нажатие что-то меняет. Раньше обе были активны
        # всегда, и по ним нельзя было понять, установлен он или нет.
        self.lbl_watcher = ctk.CTkLabel(
            tab,
            text="Проверяю ловец узлов…",
            font=F["body"],
            text_color=C["muted"],
            anchor="w",
            justify="left",
            wraplength=560,
        )
        self.lbl_watcher.pack(fill="x", pady=(0, 6))
        self.btn_watcher_install = button(
            tab,
            "Установить ловец узлов",
            lambda: self.app.on_install_watcher(False),
            style="soft",
            height=40,
        )
        self.btn_watcher_install.pack(fill="x", pady=4)
        self.btn_watcher_stop = button(
            tab,
            "Остановить ловец",
            self.app.on_stop_watcher,
            style="soft",
            icon="stop",
            height=40,
        )
        self.btn_watcher_stop.pack(fill="x", pady=4)
        self.app.lbl_watcher = self.lbl_watcher
        self.app.btn_watcher_install = self.btn_watcher_install
        self.app.btn_watcher_stop = self.btn_watcher_stop
        button(
            tab,
            "Применить маршруты",
            self.app.on_apply_routes,
            style="primary",
            icon="apply",
            height=40,
        ).pack(fill="x", pady=4)
        ctk.CTkLabel(
            tab,
            text="Та же кнопка, что на вкладке «Приложения». Нужен UAC (Да).",
            font=F["small"],
            text_color=C["muted"],
            anchor="w",
        ).pack(fill="x", pady=(0, 12))

        # Перенос настройки между компьютерами: разбирать полсотни программ по
        # каналам на каждой машине заново — работа впустую.
        ctk.CTkLabel(
            tab,
            text="Каналы приложений",
            font=F["title"],
            text_color=C["text"],
            anchor="w",
        ).pack(fill="x", pady=(4, 6))
        row = ctk.CTkFrame(tab, fg_color="transparent")
        row.pack(fill="x", pady=4)
        button(
            row,
            "Сохранить в файл",
            self.app.on_export_channels,
            style="soft",
            height=40,
        ).pack(side="left", fill="x", expand=True, padx=(0, 4))
        button(
            row,
            "Загрузить из файла",
            self.app.on_import_channels,
            style="info",
            icon="download",
            height=40,
        ).pack(side="left", fill="x", expand=True, padx=(4, 0))
        ctk.CTkLabel(
            tab,
            text="Загруженные каналы главнее уже назначенных. Работать они начнут "
            "после «Применить маршруты» — при поднятом туннеле он перезапустится, "
            "при выключенном настройка вступит в силу на старте.",
            font=F["small"],
            text_color=C["muted"],
            anchor="w",
            wraplength=430,
            justify="left",
        ).pack(fill="x", pady=(0, 8))
        button(
            tab,
            "Открыть подробный лог (применение / TUN)",
            self.app.on_open_trace_log,
            style="soft",
            height=40,
        ).pack(fill="x", pady=4)
        ctk.CTkLabel(
            tab,
            text=r"Файл: %USERPROFILE%\.claude-proxy-control\logs\cpc-trace.log",
            font=F["small"],
            text_color=C["muted"],
            anchor="w",
        ).pack(fill="x", pady=(0, 4))
