"""
Proxy God — GUI
Tabs: Live | Chain Builder | Browser | Ban Tester | Signup Prep | Privacy | Settings
(Live tab includes exit intel + world chain map)
"""
from __future__ import annotations

import logging
import ipaddress
import queue
import sys
import threading
import time
from dataclasses import asdict
from datetime import datetime
from typing import Any, Literal
from tkinter import filedialog
from urllib.parse import urlparse

import customtkinter as ctk

from .config import (
    LISTEN_HOST,
    OBFUSCATION_LABELS,
    OBFUSCATION_MODES,
    Settings,
    load_settings,
    merge_proxy_credentials,
    normalize_proxy_url,
    parse_exit_proxy_input,
    redact_proxy_url,
    save_settings,
    split_proxy_for_edit,
)
from .firewall import (
    disengage as fw_disengage,
    is_admin,
    is_engaged as fw_is_engaged,
    request_admin_relaunch,
)
from .artifact_wipe import wipe_artifacts
from .ban_tester import BanTestResult, SITE_CATEGORIES, run_ban_tests, test_single_site
from .exit_intel import ExitIntel, fetch_exit_intel
from .chain_map import build_chain_map_points, render_chain_map, resize_map_display
from .signup_prep import (
    SIGNUP_PRESET_KEYS,
    SIGNUP_PRESETS,
    AccountRecord,
    SignupDraft,
    append_account,
    generate_password,
    load_accounts,
    load_draft,
    random_first_name,
    random_full_name,
    random_last_name,
    random_username,
    resolve_signup_url,
    save_draft,
)
from .browser_identity import (
    COOKIE_LABELS,
    COOKIE_MODES,
    PERSONA_LABELS,
    PERSONAS,
)
from .dns_leak import (
    FullDnsLeakReport,
    check_dns_leak_hint,
    flush_dns_cache,
    run_dns_leak_test,
)
from .leak_audit import AuditReport, run_audit_sync
from .webrtc_check import WebRtcCheckResult, run_webrtc_check
from .browser_launcher import (
    BrowserConfig,
    BrowserSession,
    default_firefox_path,
    default_profile_dir,
)
from .fingerprint import audit_device
from .gui_validate import run_validate_each_sync
from .paths import app_data_dir
from .proxy_picker import show_proxy_picker
from .service import ChainService
from .sysproxy import clear_system_proxy, is_system_proxy_set
from .tray import TrayIcon
from .vpn_detect import detect_vpn
from .windows_task import install_logon_task, task_exists, uninstall_logon_task

LOG_PATH = app_data_dir() / "proxy_chain_manager.log"
from logging.handlers import RotatingFileHandler as _RotatingFileHandler  # noqa: E402
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    handlers=[
        _RotatingFileHandler(
            LOG_PATH, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8"
        ),
        logging.StreamHandler(sys.stdout),
    ],
)


class UILogHandler(logging.Handler):
    """Forward ``proxy_chain_manager`` log records to the Live tab (via ``ui_q``). Thread-safe."""

    def __init__(self, q: queue.Queue[dict[str, Any]]) -> None:
        super().__init__(level=logging.DEBUG)
        self._q = q

    def emit(self, record: logging.LogRecord) -> None:
        try:
            text = self.format(record)
            self._q.put_nowait({"type": "log", "text": text})
        except Exception:
            self.handleError(record)


# ── palette ──────────────────────────────────────────────────────────────────
# Neon "blueprint terminal" theme — same overall vibe, deeper black, sharper
# cyan/magenta accent, glow borders rendered via 1px border_color on cards.
BG       = "#070713"
CARD     = "#0f0f24"
PANEL    = "#171738"
ACCENT   = "#3052ff"   # primary action / outlines
ACCENT2  = "#00e5ff"   # secondary highlight / glow
GREEN    = "#00ff9c"
RED      = "#ff2a6d"
YELLOW   = "#ffd000"
ORANGE   = "#ff8a00"
PURPLE   = "#c77dff"
BLUE     = "#5b8bff"
CYAN     = "#00f5ff"
MAGENTA  = "#ff00d6"
DIM      = "#2a2f4a"
TEXT     = "#eaf2ff"
TEXT2    = "#7a8aab"
GLOW     = "#0a87ff"
FONT     = "Segoe UI"


def _ts() -> str:
    return datetime.now().strftime("%H:%M:%S")


def main() -> None:
    try:
        _main_inner()
    except Exception as _exc:  # noqa: BLE001
        _log = logging.getLogger(__name__)
        _log.exception("Unhandled exception in main loop")
        try:
            import tkinter.messagebox as _mb
            _mb.showerror(
                "Proxy God — Fatal Error",
                f"An unexpected error occurred and the application must close.\n\n"
                f"{type(_exc).__name__}: {_exc}\n\n"
                f"Details have been written to:\n{LOG_PATH}",
            )
        except Exception:
            pass
        raise


def _main_inner() -> None:
    ctk.set_appearance_mode("dark")
    ctk.set_default_color_theme("dark-blue")

    root = ctk.CTk()
    from . import __version__ as _APP_VERSION
    root.title(f"Proxy God  v{_APP_VERSION}")
    root.geometry("1080x860")
    root.minsize(900, 720)
    root.configure(fg_color=BG)

    # ── 3D-ish neon header banner (drawn on a Canvas for a true gradient) ────
    def _draw_header_banner(parent: Any) -> Any:
        import tkinter as tk
        canvas = tk.Canvas(
            parent, height=44, highlightthickness=0, bd=0, bg=BG,
        )
        canvas.pack(fill="x", side="top")

        def _hex(rgb: tuple[int, int, int]) -> str:
            return "#" + "".join(f"{v:02x}" for v in rgb)

        def _lerp(a: tuple[int, int, int], b: tuple[int, int, int], t: float) -> tuple[int, int, int]:
            return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(3))  # type: ignore[return-value]

        def _repaint(_event: Any = None) -> None:
            canvas.delete("grad")
            w = canvas.winfo_width() or 1080
            h = 44
            left = (8, 32, 64)        # near-black blue
            mid  = (48, 82, 255)      # ACCENT
            right = (0, 229, 255)     # ACCENT2 cyan
            for x in range(0, w, 2):
                t = x / max(1, w - 1)
                c = _lerp(left, mid, t * 2) if t < 0.5 else _lerp(mid, right, (t - 0.5) * 2)
                canvas.create_line(x, 0, x, h, fill=_hex(c), tags="grad")
            # 1px neon underline
            canvas.create_line(0, h - 1, w, h - 1, fill=CYAN, width=1, tags="grad")
            # Title text + small subtitle
            canvas.create_text(
                18, h // 2 - 2, anchor="w",
                text="P R O X Y   G O D",
                fill="#ffffff", font=(FONT, 16, "bold"),
                tags="grad",
            )
            canvas.create_text(
                190, h // 2 - 1, anchor="w",
                text="// multi-hop · anti-fingerprint · paranoid mode",
                fill="#bfeaff", font=(FONT, 9, "italic"),
                tags="grad",
            )

        canvas.bind("<Configure>", _repaint)
        return canvas

    _draw_header_banner(root)

    ui_q: queue.Queue[dict[str, Any]] = queue.Queue()
    svc = ChainService(notify=lambda m: ui_q.put(m))
    s = load_settings()
    browser = BrowserSession()
    browser_should_run = [False]
    browser_last_launch_ts = [0.0]
    browser_relaunch_count = [0]
    service_running = [False]

    # ── tray ─────────────────────────────────────────────────────────────────
    tray = TrayIcon(
        on_show=lambda: root.after(0, lambda: (root.deiconify(), root.lift())),
        on_quit=lambda: root.after(0, _close),
        on_rotate=svc.rotate_now,
    )
    tray.start()

    pulse_job: dict[str, str | None] = {"id": None}
    HopState = Literal["untested", "testing", "ok", "fail"]

    def _cancel_pulse() -> None:
        if pulse_job["id"]:
            try:
                root.after_cancel(pulse_job["id"])  # type: ignore[arg-type]
            except Exception:
                pass
        pulse_job["id"] = None

    def _short(u: str) -> str:
        u = redact_proxy_url(u)
        u = u.replace("http://", "").replace("socks5://", "s5://").replace("socks4://", "s4://").replace("https://", "")
        return u[:28] + "…" if len(u) > 30 else u

    def _extract_ip_host(proxy_url: str) -> str | None:
        p = urlparse(normalize_proxy_url(proxy_url))
        host = (p.hostname or "").strip()
        if not host:
            return None
        try:
            ipaddress.ip_address(host)
            return host
        except ValueError:
            return None

    # ─────────────────────────────────────────────────────────────────────────
    # HELPERS
    # ─────────────────────────────────────────────────────────────────────────
    def _btn(parent: Any, label: str, cmd: Any, w: int = 80, h: int = 28, **kw: Any) -> ctk.CTkButton:
        # Pull known keys out first so **kw never duplicates CTkButton parameters (font, width, …).
        kw.pop("text", None)
        kw.pop("command", None)
        font = kw.pop("font", (FONT, 11))
        fg_color = kw.pop("fg_color", ACCENT)
        hover_color = kw.pop("hover_color", ACCENT2)
        text_color = kw.pop("text_color", TEXT)
        corner_radius = kw.pop("corner_radius", 6)
        width = kw.pop("width", w)
        height = kw.pop("height", h)
        return ctk.CTkButton(
            parent,
            text=label,
            command=cmd,
            width=width,
            height=height,
            font=font,
            fg_color=fg_color,
            hover_color=hover_color,
            text_color=text_color,
            corner_radius=corner_radius,
            **kw,
        )

    # ─────────────────────────────────────────────────────────────────────────
    # TOP BAR
    # ─────────────────────────────────────────────────────────────────────────
    topbar = ctk.CTkFrame(root, fg_color=CARD, corner_radius=0, height=48)
    topbar.pack(fill="x", side="top")
    topbar.pack_propagate(False)

    # Status
    status_dot = ctk.CTkLabel(topbar, text="●", font=(FONT, 20), text_color=RED)
    status_dot.pack(side="left", padx=(10, 2))
    status_txt = ctk.CTkLabel(topbar, text="STOPPED", font=(FONT, 12, "bold"), text_color=TEXT)
    status_txt.pack(side="left", padx=(0, 8))

    def _start() -> None:
        _save_settings()
        # Hard-stop: if the kill-switch is enabled but we are not admin, the
        # firewall lockdown is silently skipped — which contradicts the
        # README's "fail closed" guarantee.  Refuse to start in that mode.
        try:
            from .firewall import is_admin as _is_admin
        except Exception:
            _is_admin = lambda: True  # noqa: E731
        if svc.settings.kill_switch_enabled and not _is_admin():
            from tkinter import messagebox
            if messagebox.askyesno(
                "Kill-switch requires Admin",
                "Kill-switch is enabled in Settings but this process is not "
                "running as Administrator.\n\n"
                "Continuing now will start the chain WITHOUT the firewall "
                "lockdown — your traffic will not 'fail closed' if the chain "
                "drops.\n\n"
                "Start anyway?",
            ):
                svc.start()
            return
        svc.start()

    start_btn = _btn(topbar, "▶ Start",  _start,        w=86)
    start_btn.pack(side="left", padx=2)
    stop_btn = _btn(topbar, "■ Stop",   svc.stop,       w=76)
    stop_btn.pack(side="left", padx=2)
    rotate_btn = _btn(topbar, "↻ Rotate", svc.rotate_now, w=76)
    rotate_btn.pack(side="left", padx=2)
    _btn(topbar, "Quit", lambda: root.after(0, _close), w=52,
         fg_color="#7f1d1d", hover_color="#991b1b").pack(side="left", padx=(10, 2))

    # Boot buttons
    ctk.CTkLabel(topbar, text="│", text_color=DIM).pack(side="left", padx=4)
    boot_lbl = ctk.CTkLabel(topbar, text="Boot:?", font=(FONT, 10), text_color=TEXT2)
    boot_lbl.pack(side="left", padx=2)

    def _refresh_boot() -> None:
        on = task_exists()
        boot_lbl.configure(text="Boot:ON" if on else "Boot:OFF",
                           text_color=GREEN if on else DIM)

    def _inst_boot() -> None:
        ok, msg = install_logon_task()
        _log("Boot task installed." if ok else f"Boot install: {msg}")
        _refresh_boot()

    def _rm_boot() -> None:
        ok, msg = uninstall_logon_task()
        _log("Boot task removed." if ok else f"Boot remove: {msg}")
        _refresh_boot()

    _btn(topbar, "Boot+", _inst_boot, w=54).pack(side="left", padx=2)
    _btn(topbar, "Boot−", _rm_boot,  w=54).pack(side="left", padx=2)

    # Right side indicators
    rotation_lbl = ctk.CTkLabel(topbar, text="rot: 0", font=(FONT, 10), text_color=TEXT2)
    rotation_lbl.pack(side="right", padx=(4, 12))

    countdown_lbl = ctk.CTkLabel(topbar, text="next: —", font=(FONT, 10), text_color=TEXT2)
    countdown_lbl.pack(side="right", padx=4)

    proxy_lbl = ctk.CTkLabel(topbar, text=f"{LISTEN_HOST}:{s.local_port}",
                             font=(FONT, 11, "bold"), text_color=BLUE)
    proxy_lbl.pack(side="right", padx=(4, 4))
    ctk.CTkLabel(topbar, text="proxy:", font=(FONT, 10), text_color=TEXT2).pack(side="right")

    fw_lbl = ctk.CTkLabel(topbar, text="FW:—", font=(FONT, 10), text_color=DIM)
    fw_lbl.pack(side="right", padx=6)

    sys_lbl = ctk.CTkLabel(topbar, text="SYS:—", font=(FONT, 10), text_color=DIM)
    sys_lbl.pack(side="right", padx=4)

    admin_badge = ctk.CTkLabel(
        topbar,
        text="⚡ ADMIN" if is_admin() else "👤 USER",
        font=(FONT, 10, "bold"),
        text_color=GREEN if is_admin() else YELLOW,
    )
    admin_badge.pack(side="right", padx=4)

    if not is_admin():
        def _elevate() -> None:
            # Persist UI state to disk before handing off to the elevated
            # process — otherwise unsaved edits in Chain Builder / Settings
            # are lost when this window closes.
            try:
                _save_settings()
            except Exception as exc:  # noqa: BLE001
                log.warning("Could not save settings before admin relaunch: %s", exc)
            if request_admin_relaunch():
                _close()
        _btn(topbar, "Run as Admin", _elevate, w=104,
             fg_color="#7f1d1d", hover_color="#991b1b").pack(side="right", padx=4)

    # ─────────────────────────────────────────────────────────────────────────
    # CHAIN VIZ BAR
    # ─────────────────────────────────────────────────────────────────────────
    viz_bar = ctk.CTkFrame(root, fg_color=PANEL, corner_radius=0, height=52)
    viz_bar.pack(fill="x")
    viz_bar.pack_propagate(False)

    your_ip_lbl = ctk.CTkLabel(viz_bar, text="Your IP: —", font=(FONT, 10), text_color=TEXT2)
    your_ip_lbl.pack(side="left", padx=(12, 8))

    chain_canvas = ctk.CTkFrame(viz_bar, fg_color="transparent")
    chain_canvas.pack(side="left", fill="both", expand=True)

    exit_ip_lbl = ctk.CTkLabel(viz_bar, text="Exit: —",
                               font=(FONT, 13, "bold"), text_color=YELLOW)
    exit_ip_lbl.pack(side="right", padx=(8, 14))

    hop_dot_labels: list[ctk.CTkLabel] = []
    outer_hop_label = "VPN"  # updated when VPN detected; "Direct" if no tunnel

    def _pulse(step: int = 0) -> None:
        if not hop_dot_labels:
            pulse_job["id"] = None
            return
        c = YELLOW if step % 2 == 0 else DIM
        for d in hop_dot_labels:
            d.configure(text_color=c)
        pulse_job["id"] = root.after(380, lambda s=step: _pulse(s + 1))  # type: ignore[arg-type]

    def _render_chain(
        hops: list[str], status: str, exit_ip: str | None, fixed_last: bool = False
    ) -> None:
        _cancel_pulse()
        for w in list(chain_canvas.winfo_children()):
            w.destroy()
        hop_dot_labels.clear()

        hop_c = {"healthy": GREEN, "dead": RED, "connecting": YELLOW}.get(status, DIM)

        def _node(text: str, color: str, bold: bool = False) -> None:
            ctk.CTkLabel(
                chain_canvas, text=text,
                font=(FONT, 11, "bold") if bold else (FONT, 10),
                text_color=color,
            ).pack(side="left", padx=1)

        def _arr() -> None:
            ctk.CTkLabel(chain_canvas, text="→", font=(FONT, 11), text_color=DIM).pack(side="left", padx=2)

        _node("YOU", GREEN, bold=True)
        _arr()
        _node(outer_hop_label[:12], PURPLE, bold=True)
        _arr()

        last_i = len(hops) - 1
        for i, h in enumerate(hops):
            is_fixed = fixed_last and i == last_i and last_i >= 0
            lbl_c = ORANGE if is_fixed else hop_c
            dot = ctk.CTkLabel(chain_canvas, text="●", font=(FONT, 12), text_color=lbl_c)
            dot.pack(side="left", padx=1)
            hop_dot_labels.append(dot)
            label = _short(h) + (" ★" if is_fixed else "")
            ctk.CTkLabel(chain_canvas, text=label, font=(FONT, 10), text_color=lbl_c).pack(side="left", padx=1)
            if i < len(hops) - 1:
                _arr()

        _arr()
        _node("WEB", BLUE, bold=True)

        exit_ip_lbl.configure(
            text=f"Exit: {exit_ip}" if exit_ip else "Exit: —",
            text_color=GREEN if status == "healthy" else (RED if exit_ip else YELLOW),
        )

        if status == "connecting" and hop_dot_labels:
            _pulse()

    # ─────────────────────────────────────────────────────────────────────────
    # PROGRESS STRIP
    # ─────────────────────────────────────────────────────────────────────────
    prog_strip = ctk.CTkFrame(root, fg_color=BG, height=22)
    prog_strip.pack(fill="x", padx=8, pady=(4, 0))
    prog_strip.pack_propagate(False)

    phase_lbl = ctk.CTkLabel(prog_strip, text="idle", font=(FONT, 10), text_color=TEXT2, width=70)
    phase_lbl.pack(side="left", padx=4)

    prog_bar = ctk.CTkProgressBar(prog_strip, height=8, corner_radius=4,
                                  fg_color=CARD, progress_color=BLUE)
    prog_bar.pack(side="left", fill="x", expand=True, padx=4)
    prog_bar.set(0)

    pool_lbl = ctk.CTkLabel(prog_strip, text="pool: 0", font=(FONT, 10), text_color=TEXT2, width=70)
    pool_lbl.pack(side="right", padx=4)

    # ─────────────────────────────────────────────────────────────────────────
    # TABVIEW
    # ─────────────────────────────────────────────────────────────────────────
    tabs = ctk.CTkTabview(
        root,
        fg_color=BG,
        segmented_button_fg_color=CARD,
        segmented_button_selected_color=ACCENT2,
        segmented_button_unselected_color=CARD,
        segmented_button_selected_hover_color=ACCENT,
        border_width=0,
    )
    tabs.pack(fill="both", expand=True, padx=8, pady=(4, 8))

    tab_live     = tabs.add("  Live  ")
    tab_chain    = tabs.add("  Chain Builder  ")
    tab_browser  = tabs.add("  Browser  ")
    tab_ban      = tabs.add("  Ban Tester  ")
    tab_signup   = tabs.add("  Signup Prep  ")
    tab_privacy  = tabs.add("  Privacy  ")
    tab_settings = tabs.add("  Settings  ")
    for tab in (tab_live, tab_chain, tab_browser, tab_ban, tab_signup, tab_privacy, tab_settings):
        tab.configure(fg_color=BG)

    # ─── TAB: LIVE ───────────────────────────────────────────────────────────
    # Stats cards row
    stats_row = ctk.CTkFrame(tab_live, fg_color="transparent")
    stats_row.pack(fill="x", pady=(4, 6))

    def _stat_card(parent: Any, title: str, var: ctk.StringVar, color: str = TEXT) -> None:
        f = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=8)
        f.pack(side="left", fill="x", expand=True, padx=4)
        ctk.CTkLabel(f, text=title, font=(FONT, 9), text_color=TEXT2).pack(
            anchor="w", padx=8, pady=(6, 0))
        ctk.CTkLabel(f, textvariable=var, font=(FONT, 14, "bold"), text_color=color).pack(
            anchor="w", padx=8, pady=(0, 6))

    v_real_ip  = ctk.StringVar(value="—")
    v_exit_ip  = ctk.StringVar(value="—")
    v_pool     = ctk.StringVar(value="0")
    v_rotation = ctk.StringVar(value="0")

    _stat_card(stats_row, "Direct / VPN IP", v_real_ip)
    _stat_card(stats_row, "Exit IP  (last hop)", v_exit_ip)
    _stat_card(stats_row, "Valid pool size", v_pool)
    _stat_card(stats_row, "Rotations this session", v_rotation)

    # ── Exit IP intel card (geo / ASN / datacenter) ──────────────────────────
    intel_card = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8)
    intel_card.pack(fill="x", pady=(0, 6))

    intel_head = ctk.CTkFrame(intel_card, fg_color="transparent")
    intel_head.pack(fill="x", padx=10, pady=(8, 2))
    ctk.CTkLabel(
        intel_head, text="Exit IP intel", font=(FONT, 11, "bold"), text_color=CYAN,
    ).pack(side="left")

    v_intel_loc = ctk.StringVar(value="Location: —")
    v_intel_asn = ctk.StringVar(value="ASN / ISP: —")
    v_intel_tags = ctk.StringVar(value="Flags: —")

    intel_body = ctk.CTkFrame(intel_card, fg_color="transparent")
    intel_body.pack(fill="x", padx=10, pady=(0, 8))
    ctk.CTkLabel(intel_body, textvariable=v_intel_loc, font=(FONT, 11), text_color=TEXT,
                 anchor="w").pack(anchor="w")
    ctk.CTkLabel(intel_body, textvariable=v_intel_asn, font=(FONT, 11), text_color=TEXT2,
                 anchor="w").pack(anchor="w")
    intel_tags_lbl = ctk.CTkLabel(intel_body, textvariable=v_intel_tags, font=(FONT, 11, "bold"),
                                  text_color=GREEN, anchor="w")
    intel_tags_lbl.pack(anchor="w")

    intel_running = [False]
    last_intel: dict[str, ExitIntel | None] = {"intel": None}

    def _apply_intel(intel: ExitIntel | None) -> None:
        last_intel["intel"] = intel
        if intel is None or not intel.ok:
            v_intel_loc.set("Location: —")
            v_intel_asn.set("ASN / ISP: —")
            v_intel_tags.set("Flags: " + (intel.detail if intel and intel.detail else "—"))
            intel_tags_lbl.configure(text_color=TEXT2)
            return
        loc_parts = [p for p in (intel.city, intel.region, intel.country) if p]
        v_intel_loc.set("Location: " + (", ".join(loc_parts) if loc_parts else "—"))
        asn_str = f"AS{intel.asn} " if intel.asn else ""
        org_str = intel.org or intel.isp or "—"
        v_intel_asn.set(f"ASN / ISP: {asn_str}{org_str}")
        tags: list[str] = []
        color = GREEN
        if intel.is_datacenter:
            tags.append("DATACENTER")
            color = ORANGE
        if intel.is_proxy_flagged:
            tags.append("PROXY-FLAGGED")
            color = RED
        if intel.is_mobile:
            tags.append("MOBILE")
        if not tags:
            tags.append("residential-like")
            color = GREEN
        v_intel_tags.set("Flags: " + " · ".join(tags))
        intel_tags_lbl.configure(text_color=color)
        root.after(200, lambda: _refresh_chain_map(silent=True))

    def _refresh_exit_intel(silent: bool = False) -> None:
        if intel_running[0]:
            return
        if not service_running[0] or not svc.current_chain:
            if not silent:
                _log("Exit intel: chain not running.")
            _apply_intel(None)
            return
        intel_running[0] = True
        v_intel_tags.set("Flags: looking up…")
        intel_tags_lbl.configure(text_color=YELLOW)
        proxy_url = f"http://{svc.settings.listen_addr()}"

        def work() -> None:
            intel = fetch_exit_intel(
                proxy_url,
                timeout_seconds=min(15.0, svc.settings.validation_timeout_seconds + 3.0),
            )

            def done() -> None:
                _apply_intel(intel)
                intel_running[0] = False
                if not silent:
                    if intel.ok:
                        _log(f"Exit intel: {intel.summary()} (via {intel.source})")
                    else:
                        _log(f"Exit intel: lookup failed — {intel.detail}")

            root.after(0, done)

        threading.Thread(target=work, daemon=True).start()

    intel_btn_row = ctk.CTkFrame(intel_card, fg_color="transparent")
    intel_btn_row.pack(fill="x", padx=10, pady=(0, 8))
    _btn(intel_btn_row, "Refresh intel", lambda: _refresh_exit_intel(False), w=110, h=24,
         fg_color=ACCENT2, hover_color=ACCENT).pack(side="left")

    # ── Chain world map ───────────────────────────────────────────────────────
    map_card = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8)
    map_card.pack(fill="x", pady=(0, 6))

    map_head = ctk.CTkFrame(map_card, fg_color="transparent")
    map_head.pack(fill="x", padx=10, pady=(8, 2))
    ctk.CTkLabel(
        map_head, text="Chain map", font=(FONT, 11, "bold"), text_color=CYAN,
    ).pack(side="left")
    v_map_status = ctk.StringVar(value="Plotting: waiting for chain…")
    ctk.CTkLabel(
        map_head, textvariable=v_map_status, font=(FONT, 10), text_color=TEXT2,
    ).pack(side="right")

    map_frame = ctk.CTkFrame(map_card, fg_color=BG, corner_radius=6)
    map_frame.pack(fill="x", padx=10, pady=(0, 8))
    map_img_label = ctk.CTkLabel(map_frame, text="")
    map_img_label.pack(padx=2, pady=2)

    map_ctk_image: dict[str, ctk.CTkImage | None] = {"img": None}
    last_chain_state: dict[str, Any] = {
        "hops": [],
        "status": "idle",
        "exit_ip": "",
        "direct_ip": "",
    }
    map_running = [False]

    def _show_chain_map(pil_img: Any) -> None:
        try:
            map_frame.update_idletasks()
            w = max(640, map_frame.winfo_width() - 8)
            h = max(180, min(240, int(w * 0.23)))
            resized = resize_map_display(pil_img, w, h)
            map_ctk_image["img"] = ctk.CTkImage(
                light_image=resized, dark_image=resized, size=(w, h),
            )
            map_img_label.configure(image=map_ctk_image["img"], text="")
        except Exception as e:
            logging.getLogger(__name__).debug("chain map display: %s", e)

    def _refresh_chain_map(silent: bool = True) -> None:
        if map_running[0]:
            return
        hops = list(last_chain_state["hops"])
        status = str(last_chain_state["status"] or "idle")
        if not service_running[0] or not hops:
            v_map_status.set("Plotting: chain not running")
            _show_chain_map(render_chain_map([], status="idle"))
            return
        map_running[0] = True
        v_map_status.set("Plotting: resolving hop locations…")
        direct_ip = str(last_chain_state.get("direct_ip") or "")
        exit_ip = str(last_chain_state.get("exit_ip") or "")
        exit_intel = last_intel.get("intel")
        timeout = min(12.0, svc.settings.validation_timeout_seconds + 2.0)

        def work() -> None:
            points = build_chain_map_points(
                hops,
                direct_ip=direct_ip,
                exit_ip=exit_ip,
                exit_intel=exit_intel if isinstance(exit_intel, ExitIntel) else None,
                timeout_seconds=timeout,
            )
            img = render_chain_map(points, width=960, height=220, status=status)  # type: ignore[arg-type]

            def done() -> None:
                _show_chain_map(img)
                if points:
                    v_map_status.set(f"Plotting: {len(points)} nodes · {status}")
                    if not silent:
                        _log(f"Chain map updated — {len(points)} geo nodes ({status}).")
                else:
                    v_map_status.set("Plotting: no geo data yet (hop DNS/geo pending)")
                map_running[0] = False

            root.after(0, done)

        threading.Thread(target=work, daemon=True).start()

    _btn(map_head, "Refresh map", lambda: _refresh_chain_map(False), w=100, h=22,
         fg_color=ACCENT2, hover_color=ACCENT).pack(side="right", padx=(8, 0))

    _show_chain_map(render_chain_map([], status="idle"))

    # Log box
    log_frame = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8)
    log_frame.pack(fill="both", expand=True)
    log_box = ctk.CTkTextbox(
        log_frame, font=("Consolas", 10),
        fg_color=BG, text_color=TEXT,
        scrollbar_button_color=ACCENT,
    )
    log_box.pack(fill="both", expand=True, padx=4, pady=(0, 4))

    def _log(line: str) -> None:
        log_box.insert("end", f"[{_ts()}] {line}\n")
        log_box.see("end")

    log_tool = ctk.CTkFrame(log_frame, fg_color="transparent")
    log_tool.pack(fill="x", padx=8, pady=(6, 0), before=log_box)
    verbose_logs_var = ctk.BooleanVar(value=True)
    _pcm_logger = logging.getLogger("proxy_chain_manager")
    _ui_log_handler: UILogHandler | None = None
    for _h in _pcm_logger.handlers:
        if isinstance(_h, UILogHandler):
            _ui_log_handler = _h
            break
    if _ui_log_handler is None:
        _ui_log_handler = UILogHandler(ui_q)
        _ui_log_handler.setFormatter(
            logging.Formatter("%(levelname)-5s [%(name)s] %(message)s")
        )
        _pcm_logger.addHandler(_ui_log_handler)
    logging.getLogger("httpx").setLevel(logging.WARNING)
    logging.getLogger("httpcore").setLevel(logging.WARNING)

    def _sync_ui_log_level() -> None:
        on = bool(verbose_logs_var.get())
        lvl = logging.DEBUG if on else logging.INFO
        _pcm_logger.setLevel(lvl)
        if _ui_log_handler is not None:
            _ui_log_handler.setLevel(lvl)
        _log(f"UI log level: {'DEBUG (verbose)' if on else 'INFO'}")

    ctk.CTkCheckBox(
        log_tool,
        text="Verbose logs (DEBUG in UI — file stays INFO)",
        variable=verbose_logs_var,
        command=_sync_ui_log_level,
        font=(FONT, 11),
        fg_color=ACCENT2,
        hover_color=ACCENT,
        text_color=TEXT,
    ).pack(side="left", padx=(0, 12))
    _btn(log_tool, "Clear log", lambda: log_box.delete("1.0", "end"), w=88, h=26,
         fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6))
    _sync_ui_log_level()

    # ─── TAB: CHAIN BUILDER ──────────────────────────────────────────────────
    cb_split = ctk.CTkFrame(tab_chain, fg_color="transparent")
    cb_split.pack(fill="x", pady=(4, 8))

    # Left — mode + hop slider
    cb_left = ctk.CTkFrame(cb_split, fg_color=CARD, corner_radius=8)
    cb_left.pack(side="left", fill="both", expand=True, padx=(0, 6))

    ctk.CTkLabel(cb_left, text="Obfuscation Mode", font=(FONT, 12, "bold"),
                 text_color=TEXT).pack(anchor="w", padx=12, pady=(12, 4))

    mode_var = ctk.StringVar(value=s.obfuscation_mode)
    for mk in OBFUSCATION_MODES:
        ctk.CTkRadioButton(
            cb_left,
            text=OBFUSCATION_LABELS[mk],
            variable=mode_var,
            value=mk,
            font=(FONT, 11),
            fg_color=ACCENT2,
            hover_color=ACCENT,
            text_color=TEXT,
        ).pack(anchor="w", padx=16, pady=3)

    elite_var = ctk.BooleanVar(value=s.prefer_elite)
    ctk.CTkCheckBox(
        cb_left, text="Elite proxies only (slower but more anonymous)",
        variable=elite_var, font=(FONT, 11),
        fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT,
    ).pack(anchor="w", padx=16, pady=(0, 8))

    exit_fix_frame = ctk.CTkFrame(cb_left, fg_color=PANEL, corner_radius=8)
    exit_fix_frame.pack(fill="x", padx=12, pady=(0, 12))
    exit_hdr = ctk.CTkFrame(exit_fix_frame, fg_color="transparent")
    exit_hdr.pack(fill="x", padx=10, pady=(8, 2))
    ctk.CTkLabel(
        exit_hdr,
        text="Exit node (optional final hop)",
        font=(FONT, 11, "bold"),
        text_color=ORANGE,
    ).pack(side="left")
    exit_test_dot = ctk.CTkLabel(exit_hdr, text="●", font=(FONT, 14), text_color=RED)
    exit_test_dot.pack(side="left", padx=(8, 4))
    exit_proxy_entry = ctk.CTkEntry(
        exit_fix_frame,
        placeholder_text="host:port:user:pass   (defaults to socks5 — paste any format)",
        font=("Consolas", 10),
        fg_color=BG,
        border_color=ACCENT,
        height=28,
    )
    exit_proxy_entry.pack(fill="x", padx=10, pady=(0, 4))
    exit_auth_row = ctk.CTkFrame(exit_fix_frame, fg_color="transparent")
    exit_auth_row.pack(fill="x", padx=10, pady=(0, 4))
    ctk.CTkLabel(exit_auth_row, text="User", font=(FONT, 10), text_color=TEXT2, width=36).pack(
        side="left", padx=(0, 4)
    )
    exit_user_entry = ctk.CTkEntry(
        exit_auth_row, width=120, height=26, font=("Consolas", 10),
        fg_color=BG, border_color=ACCENT, placeholder_text="optional",
    )
    exit_user_entry.pack(side="left", padx=(0, 10))
    ctk.CTkLabel(exit_auth_row, text="Pass", font=(FONT, 10), text_color=TEXT2, width=36).pack(
        side="left", padx=(0, 4)
    )
    exit_pass_entry = ctk.CTkEntry(
        exit_auth_row, width=120, height=26, font=("Consolas", 10),
        fg_color=BG, border_color=ACCENT, placeholder_text="optional", show="*",
    )
    exit_pass_entry.pack(side="left", padx=(0, 0))

    _eb, _eu, _epw = split_proxy_for_edit(s.manual_exit_proxy)
    if _eb:
        exit_proxy_entry.insert(0, _eb)
    if _eu:
        exit_user_entry.insert(0, _eu)
    if _epw:
        exit_pass_entry.insert(0, _epw)

    exit_btn_row = ctk.CTkFrame(exit_fix_frame, fg_color="transparent")
    exit_btn_row.pack(fill="x", padx=10, pady=(0, 4))

    def _auto_split_exit_paste(_event: Any | None = None) -> None:
        """If the URL field looks like ``host:port:user:pass``, distribute
        the user/pass parts into their own fields so the user sees what got
        parsed. Triggered on key release and explicit paste.
        """
        raw = exit_proxy_entry.get()
        if "://" in raw:
            return
        bits = [b for b in raw.strip().split(":") if b != ""]
        if len(bits) < 4 and "@" not in raw and not any(c in raw for c in (" ", "\t")):
            return
        base, user, pw = parse_exit_proxy_input(raw, default_scheme="socks5")
        if not base:
            return
        if base != raw:
            exit_proxy_entry.delete(0, "end")
            exit_proxy_entry.insert(0, base)
        if user and not exit_user_entry.get().strip():
            exit_user_entry.delete(0, "end")
            exit_user_entry.insert(0, user)
        if pw and not exit_pass_entry.get().strip():
            exit_pass_entry.delete(0, "end")
            exit_pass_entry.insert(0, pw)

    exit_proxy_entry.bind("<KeyRelease>", _auto_split_exit_paste)
    exit_proxy_entry.bind("<<Paste>>", lambda e: exit_proxy_entry.after(1, _auto_split_exit_paste))
    exit_proxy_entry.bind("<FocusOut>", _auto_split_exit_paste)

    def _exit_url() -> str:
        """Build the final exit-proxy URL.

        Accepts every paste format ``parse_exit_proxy_input`` understands
        (host:port:user:pass, user:pass@host:port, scheme://…). Defaults to
        SOCKS5 when no scheme is supplied.
        """
        base, parsed_user, parsed_pw = parse_exit_proxy_input(
            exit_proxy_entry.get(), default_scheme="socks5"
        )
        if not base:
            return ""
        user = (exit_user_entry.get() or "").strip() or parsed_user
        pw = (exit_pass_entry.get() or "").strip() or parsed_pw
        return merge_proxy_credentials(base, user, pw)

    def _test_exit() -> None:
        url = _exit_url()
        if not url:
            _log("Exit node: enter a proxy URL first.")
            return
        exit_test_dot.configure(text_color=YELLOW)
        test_exit_btn.configure(state="disabled")

        def work() -> None:
            def on_result(_u: str, ok: bool) -> None:
                root.after(0, lambda: exit_test_dot.configure(text_color=GREEN if ok else RED))
                root.after(0, lambda: _log(
                    f"Exit node: {'OK' if ok else 'FAILED'} — {_short(url)}"
                ))

            run_validate_each_sync(
                [url],
                svc.settings.ip_check_url,
                min(12.0, svc.settings.validation_timeout_seconds),
                on_result,
                concurrency=1,
            )
            root.after(0, lambda: test_exit_btn.configure(state="normal"))

        threading.Thread(target=work, daemon=True).start()

    test_exit_btn = _btn(exit_btn_row, "Test exit", _test_exit, w=88, h=26,
                         fg_color=ACCENT2, hover_color=ACCENT)
    test_exit_btn.pack(side="left")

    def _save_exit_to_chain() -> None:
        """Append the typed exit proxy to the manual chain as the final hop."""
        url = _exit_url()
        if not url:
            _log("Exit node: paste a host:port[:user:pass] first.")
            return
        # If the URL is already in the chain, move it to the bottom rather than dup.
        if url in manual_chain:
            manual_chain.remove(url)
        manual_chain.append(url)
        hop_status[url] = "ok" if (
            exit_test_dot.cget("text_color") == GREEN
        ) else "untested"
        use_manual_var.set(True)
        _rebuild_chain_ui()
        exit_proxy_entry.delete(0, "end")
        exit_user_entry.delete(0, "end")
        exit_pass_entry.delete(0, "end")
        exit_test_dot.configure(text_color=RED)
        _log(f"Exit node added as final hop ({len(manual_chain)} total).")

    _btn(exit_btn_row, "Save as final hop", _save_exit_to_chain, w=140, h=26,
         fg_color=GREEN, hover_color="#00b377").pack(side="left", padx=(6, 0))

    ctk.CTkLabel(
        exit_fix_frame,
        text=(
            "Test exit, then 'Save as final hop' appends it to YOUR CHAIN above as the last proxy.\n"
            "Paste any of:  host:port:user:pass  •  user:pass@host:port  •  scheme://host:port\n"
            "No scheme = SOCKS5. Paste auto-splits into User/Pass below."
        ),
        font=(FONT, 9),
        text_color=TEXT2,
        justify="left",
    ).pack(anchor="w", padx=10, pady=(0, 8))

    # Right — your chain
    cb_right = ctk.CTkFrame(cb_split, fg_color=CARD, corner_radius=8, width=400)
    cb_right.pack(side="left", fill="both", expand=True)

    hdr_row = ctk.CTkFrame(cb_right, fg_color="transparent")
    hdr_row.pack(fill="x", padx=12, pady=(12, 2))
    ctk.CTkLabel(
        hdr_row, text="Your chain",
        font=(FONT, 12, "bold"), text_color=TEXT,
    ).pack(side="left")
    chain_count_lbl = ctk.CTkLabel(hdr_row, text="", font=(FONT, 10), text_color=TEXT2)
    chain_count_lbl.pack(side="left", padx=(8, 0))

    use_manual_var = ctk.BooleanVar(value=True if s.pinned_chain else s.use_pinned_chain)
    use_chain_cb = ctk.CTkCheckBox(
        hdr_row, text="Use this chain on Start",
        variable=use_manual_var, font=(FONT, 10),
        fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT2,
    )
    use_chain_cb.pack(side="right")

    ctk.CTkLabel(
        cb_right,
        text="Traffic flows top → bottom. ● red = not tested  ● green = live",
        font=(FONT, 9), text_color=TEXT2,
    ).pack(anchor="w", padx=12, pady=(0, 4))

    manual_chain: list[str] = list(s.pinned_chain)
    hop_status: dict[str, HopState] = {u: "untested" for u in manual_chain}
    status_dots: dict[str, ctk.CTkLabel] = {}

    _HOP_COLOR = {"untested": RED, "testing": YELLOW, "ok": GREEN, "fail": RED}

    chain_list_frame = ctk.CTkScrollableFrame(
        cb_right, fg_color=BG, height=200, corner_radius=6,
        scrollbar_button_color=ACCENT,
    )
    chain_list_frame.pack(fill="x", padx=12, pady=(0, 4))

    def _update_chain_count() -> None:
        n = len(manual_chain)
        chain_count_lbl.configure(text=f"({n} hop{'s' if n != 1 else ''})")

    def _rebuild_chain_ui() -> None:
        status_dots.clear()
        for w in list(chain_list_frame.winfo_children()):
            w.destroy()
        for i, hop in enumerate(manual_chain):
            if hop not in hop_status:
                hop_status[hop] = "untested"
            _chain_row(i, hop)
        _update_chain_count()

    def _chain_row(idx: int, hop: str) -> None:
        fr = ctk.CTkFrame(chain_list_frame, fg_color=PANEL, corner_radius=6, height=32)
        fr.pack(fill="x", pady=2)
        fr.pack_propagate(False)

        st = hop_status.get(hop, "untested")
        dot = ctk.CTkLabel(fr, text="●", font=(FONT, 12),
                           text_color=_HOP_COLOR[st], width=18)
        dot.pack(side="left", padx=(4, 0))
        status_dots[hop] = dot

        ctk.CTkLabel(fr, text=f"{idx+1}", font=(FONT, 10, "bold"),
                     text_color=CYAN, width=22).pack(side="left")

        proto = hop.split("://")[0].upper() if "://" in hop else "HTTP"
        badge_c = {"HTTP": "#1e3a8a", "SOCKS5": "#064e3b", "SOCKS4": "#3b1a64"}.get(proto, DIM)
        ctk.CTkLabel(fr, text=proto, font=(FONT, 9, "bold"),
                     fg_color=badge_c, corner_radius=4, padx=4,
                     text_color=TEXT).pack(side="left", padx=2)

        ctk.CTkLabel(fr, text=_short(hop), font=("Consolas", 10),
                     text_color=TEXT, anchor="w").pack(side="left", fill="x", expand=True, padx=4)

        def _up(i: int = idx) -> None:
            if i > 0:
                manual_chain[i], manual_chain[i-1] = manual_chain[i-1], manual_chain[i]
                _rebuild_chain_ui()

        def _dn(i: int = idx) -> None:
            if i < len(manual_chain) - 1:
                manual_chain[i], manual_chain[i+1] = manual_chain[i+1], manual_chain[i]
                _rebuild_chain_ui()

        def _rm(i: int = idx) -> None:
            removed = manual_chain[i]
            del manual_chain[i]
            hop_status.pop(removed, None)
            _rebuild_chain_ui()

        _btn(fr, "↑", _up, w=26, h=24, fg_color=DIM, hover_color=ACCENT).pack(side="right", padx=1)
        _btn(fr, "↓", _dn, w=26, h=24, fg_color=DIM, hover_color=ACCENT).pack(side="right", padx=1)
        _btn(fr, "✕", _rm, w=26, h=24, fg_color="#7f1d1d", hover_color=RED).pack(side="right", padx=(1, 6))

    _rebuild_chain_ui()

    test_row = ctk.CTkFrame(cb_right, fg_color="transparent")
    test_row.pack(fill="x", padx=12, pady=(0, 6))

    def _set_all_status(state: HopState) -> None:
        for u in manual_chain:
            hop_status[u] = state
            if u in status_dots:
                status_dots[u].configure(text_color=_HOP_COLOR[state])

    def _test_chain() -> None:
        if not manual_chain:
            _log("Add proxies to your chain first.")
            return
        test_chain_btn.configure(state="disabled")
        _set_all_status("testing")
        check_url = svc.settings.ip_check_url
        timeout = min(12.0, svc.settings.validation_timeout_seconds)
        ok_count = [0]

        def on_result(url: str, ok: bool) -> None:
            hop_status[url] = "ok" if ok else "fail"
            if ok:
                ok_count[0] += 1

            def ui() -> None:
                if url in status_dots:
                    status_dots[url].configure(text_color=GREEN if ok else RED)

            root.after(0, ui)

        def work() -> None:
            run_validate_each_sync(manual_chain, check_url, timeout, on_result)
            n_ok, n_tot = ok_count[0], len(manual_chain)

            def done() -> None:
                test_chain_btn.configure(state="normal")
                _log(f"Chain test: {n_ok}/{n_tot} proxies responded OK.")

            root.after(0, done)

        threading.Thread(target=work, daemon=True).start()

    test_chain_btn = _btn(test_row, "▶ Test entire chain", _test_chain, w=150, h=28,
                          fg_color=ACCENT2, hover_color=ACCENT)
    test_chain_btn.pack(side="left", padx=(0, 8))

    # Add proxy input
    add_row = ctk.CTkFrame(cb_right, fg_color="transparent")
    add_row.pack(fill="x", padx=12, pady=(0, 4))
    add_entry = ctk.CTkEntry(
        add_row,
        placeholder_text="http://host:port  or  user:pass@host:port",
        font=(FONT, 10), fg_color=BG, border_color=ACCENT, height=28,
    )
    add_entry.pack(side="left", fill="x", expand=True, padx=(0, 4))
    add_user_entry = ctk.CTkEntry(
        add_row, width=88, height=28, font=("Consolas", 10),
        fg_color=BG, border_color=ACCENT, placeholder_text="user",
    )
    add_user_entry.pack(side="left", padx=(0, 2))
    add_pass_entry = ctk.CTkEntry(
        add_row, width=88, height=28, font=("Consolas", 10),
        fg_color=BG, border_color=ACCENT, placeholder_text="pass", show="*",
    )
    add_pass_entry.pack(side="left", padx=(0, 4))

    def _add_proxy() -> None:
        v = add_entry.get().strip()
        if not v:
            return
        if "://" not in v:
            v = "http://" + v
        au = add_user_entry.get().strip()
        ap = add_pass_entry.get().strip()
        if au or ap:
            v = merge_proxy_credentials(v, au, ap)
        else:
            v = normalize_proxy_url(v)
        manual_chain.append(v)
        hop_status[v] = "untested"
        _rebuild_chain_ui()
        add_entry.delete(0, "end")
        add_user_entry.delete(0, "end")
        add_pass_entry.delete(0, "end")

    _btn(add_row, "+ Add", _add_proxy, w=68, h=28).pack(side="left")

    chain_btn_row = ctk.CTkFrame(cb_right, fg_color="transparent")
    chain_btn_row.pack(pady=(0, 10))

    def _on_picker_done(urls: list[str], mode: str) -> None:
        if mode == "replace":
            manual_chain.clear()
            hop_status.clear()
        for u in urls:
            if u not in manual_chain:
                manual_chain.append(u)
                hop_status[u] = "untested"
        _rebuild_chain_ui()
        _log(f"{'Replaced' if mode == 'replace' else 'Added'} {len(urls)} proxies to your chain.")

    def _open_picker() -> None:
        _save_settings()
        show_proxy_picker(root, svc.settings, bool(elite_var.get()), _on_picker_done)

    browse_btn = _btn(chain_btn_row, "Browse proxy lists…", _open_picker, w=150, h=26,
         fg_color=ACCENT2, hover_color=ACCENT)
    browse_btn.pack(side="left")

    # Sources
    src_frame = ctk.CTkFrame(tab_chain, fg_color=CARD, corner_radius=8)
    src_frame.pack(fill="x", pady=(0, 6))
    ctk.CTkLabel(src_frame, text="Proxy Sources  (one JSON URL per line)",
                 font=(FONT, 11, "bold"), text_color=TEXT).pack(
        anchor="w", padx=12, pady=(8, 4))
    sources_box = ctk.CTkTextbox(
        src_frame, height=72, font=("Consolas", 10),
        fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT,
    )
    sources_box.pack(fill="x", padx=12, pady=(0, 8))
    sources_box.insert("end", "\n".join(s.sources))

    # ─── TAB: BROWSER ─────────────────────────────────────────────────────────
    br_scroll = ctk.CTkScrollableFrame(tab_browser, fg_color=BG, scrollbar_button_color=ACCENT)
    br_scroll.pack(fill="both", expand=True, padx=4, pady=4)

    def _br_section(title: str, subtitle: str = "") -> ctk.CTkFrame:
        ctk.CTkLabel(br_scroll, text=title, font=(FONT, 13, "bold"), text_color=CYAN).pack(
            anchor="w", padx=8, pady=(12, 0)
        )
        if subtitle:
            ctk.CTkLabel(
                br_scroll, text=subtitle, font=(FONT, 10), text_color=TEXT2, wraplength=900, justify="left"
            ).pack(anchor="w", padx=8, pady=(0, 4))
        f = ctk.CTkFrame(
            br_scroll, fg_color=CARD, corner_radius=10,
            border_width=1, border_color=GLOW,
        )
        f.pack(fill="x", padx=8, pady=(4, 8))
        return f

    browser_status_card = _br_section(
        "Hardened Firefox",
        "Launches Firefox in an app-controlled profile that enforces proxy/hardening prefs on every launch.",
    )
    browser_status_lbl = ctk.CTkLabel(
        browser_status_card, text="Browser status: stopped", font=(FONT, 12, "bold"), text_color=TEXT2
    )
    browser_status_lbl.pack(anchor="w", padx=12, pady=(10, 2))
    browser_hint_lbl = ctk.CTkLabel(
        browser_status_card, text="Exit IP on hover in tray while chain is healthy.", font=(FONT, 10), text_color=TEXT2
    )
    browser_hint_lbl.pack(anchor="w", padx=12, pady=(0, 10))

    browser_path_card = _br_section("Executable and profile", "Set your Firefox path once; profile is kept isolated.")
    path_row = ctk.CTkFrame(browser_path_card, fg_color="transparent")
    path_row.pack(fill="x", padx=12, pady=(10, 4))
    ctk.CTkLabel(path_row, text="Firefox exe", width=90, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left")
    firefox_path_var = ctk.StringVar(value=s.firefox_path or default_firefox_path())
    firefox_path_entry = ctk.CTkEntry(path_row, textvariable=firefox_path_var, height=28, fg_color=BG, border_color=ACCENT)
    firefox_path_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))

    def _pick_firefox_path() -> None:
        p = filedialog.askopenfilename(
            title="Select firefox.exe",
            filetypes=[("Firefox", "firefox.exe"), ("Executable", "*.exe"), ("All files", "*.*")],
        )
        if p:
            firefox_path_var.set(p)

    _btn(path_row, "Browse", _pick_firefox_path, w=72, h=28, fg_color=DIM, hover_color=ACCENT).pack(side="left")

    profile_row = ctk.CTkFrame(browser_path_card, fg_color="transparent")
    profile_row.pack(fill="x", padx=12, pady=(0, 10))
    ctk.CTkLabel(profile_row, text="Profile dir", width=90, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left")
    firefox_profile_var = ctk.StringVar(value=s.firefox_profile_dir or default_profile_dir())
    firefox_profile_entry = ctk.CTkEntry(
        profile_row, textvariable=firefox_profile_var, height=28, fg_color=BG, border_color=ACCENT
    )
    firefox_profile_entry.pack(side="left", fill="x", expand=True)

    browser_toggles_card = _br_section(
        "Browser controls",
        "These settings are applied each time you launch from this tab.",
    )
    persona_card = _section_inline(
        br_scroll,
        "Identity persona  (spoof when possible, block when not)",
        "Blend-in personas mimic the most common Windows + Chrome setup so the "
        "browser looks normal. Hardened mode is the classic 'block everything' "
        "profile but stands out as anti-fingerprint.",
    ) if "_section_inline" in dir() else None
    if persona_card is None:
        persona_card = ctk.CTkFrame(
            br_scroll, fg_color=CARD, corner_radius=10,
            border_width=1, border_color=GLOW,
        )
        persona_card.pack(fill="x", padx=8, pady=(2, 8))
        ctk.CTkLabel(
            persona_card,
            text="Identity persona  (spoof when possible, block when not)",
            font=(FONT, 12, "bold"),
            text_color=ORANGE,
        ).pack(anchor="w", padx=12, pady=(10, 0))
        ctk.CTkLabel(
            persona_card,
            text=(
                "Blend-in personas mimic the most common Windows + Chrome setup "
                "so the browser looks normal. Hardened = full anti-fingerprint."
            ),
            font=(FONT, 9), text_color=TEXT2, justify="left", wraplength=820,
        ).pack(anchor="w", padx=12, pady=(0, 6))

    persona_row = ctk.CTkFrame(persona_card, fg_color="transparent")
    persona_row.pack(fill="x", padx=12, pady=(0, 6))
    ctk.CTkLabel(persona_row, text="Persona:", font=(FONT, 11), text_color=TEXT,
                 width=80, anchor="w").pack(side="left")
    persona_var = ctk.StringVar(
        value=PERSONA_LABELS.get(s.browser_persona, PERSONA_LABELS["blend_windows_chrome"])
    )
    persona_menu = ctk.CTkOptionMenu(
        persona_row,
        values=[PERSONA_LABELS[k] for k in PERSONAS],
        variable=persona_var,
        fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT,
        text_color=TEXT, font=(FONT, 11), width=500,
    )
    persona_menu.pack(side="left", padx=(4, 0))

    cookie_row = ctk.CTkFrame(persona_card, fg_color="transparent")
    cookie_row.pack(fill="x", padx=12, pady=(0, 4))
    ctk.CTkLabel(cookie_row, text="Cookies:", font=(FONT, 11), text_color=TEXT,
                 width=80, anchor="w").pack(side="left")
    _default_cookie_label = COOKIE_LABELS.get(
        s.browser_cookie_mode, COOKIE_LABELS["block_third_party"]
    )
    cookie_var = ctk.StringVar(value=_default_cookie_label)

    # Cookie detail sub-labels (key → short description of side effects)
    _COOKIE_NOTES: dict[str, str] = {
        "accept_all":                "Everything works · cookies persist to disk",
        "accept_all_session":        "Full compat while browsing · cookies wiped on close",
        "block_third_party":         "Blocks cross-site trackers · most sites work · cookies persist",
        "block_third_party_session": "Blocks 3rd-party + nothing persists after close",
        "block_third_party_trackers":"ETP Strict — same as Firefox enhanced tracking protection",
        "block_social_trackers":     "Blocks Facebook pixel, Twitter widgets, social SSO badges",
        "block_unvisited":           "Very light — only blocks sites you haven't visited",
        "partitioned_tcp":           "Total Cookie Protection — each site gets isolated 3rd-party jar",
        "partitioned_session":       "Isolated jars + full disk wipe on close · zero cross-site state",
        "session_only":              "All cookies expire on close · 1st-party allowed while browsing",
        "ghost_mode":                "Isolated + session + wipes cache/history/localStorage/formdata",
        "block_all":                 "Breaks most logins · read-only / scraping use only",
    }

    cookie_note_lbl = ctk.CTkLabel(
        persona_card, text=_COOKIE_NOTES.get(s.browser_cookie_mode, ""),
        font=(FONT, 9), text_color=TEXT2, anchor="w", wraplength=820,
    )

    def _on_cookie_change(_choice: str = "") -> None:
        key = _cookie_key()
        cookie_note_lbl.configure(text=_COOKIE_NOTES.get(key, ""))

    cookie_menu = ctk.CTkOptionMenu(
        cookie_row,
        values=[COOKIE_LABELS[k] for k in COOKIE_MODES],
        variable=cookie_var,
        fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT,
        text_color=TEXT, font=(FONT, 11), width=680,
        command=_on_cookie_change,
    )
    cookie_menu.pack(side="left", padx=(4, 0))
    cookie_note_lbl.pack(anchor="w", padx=(92, 12), pady=(0, 10))

    # Build a label→key lookup ONCE so the reverse mapping doesn't break
    # when a label changes copy.  Falls through to the configured default if
    # the label string somehow no longer exists in the dropdown.
    _PERSONA_LABEL_TO_KEY = {PERSONA_LABELS[k]: k for k in PERSONAS}
    _COOKIE_LABEL_TO_KEY = {COOKIE_LABELS[k]: k for k in COOKIE_MODES}

    def _persona_key() -> str:
        return _PERSONA_LABEL_TO_KEY.get(persona_var.get(), "blend_windows_chrome")

    def _cookie_key() -> str:
        return _COOKIE_LABEL_TO_KEY.get(cookie_var.get(), "block_third_party")

    br_force_proxy_var = ctk.BooleanVar(value=s.browser_force_proxy)
    br_disable_webrtc_var = ctk.BooleanVar(value=s.browser_disable_webrtc)
    br_rfp_var = ctk.BooleanVar(value=s.browser_resist_fingerprinting)
    br_telemetry_var = ctk.BooleanVar(value=s.browser_disable_telemetry)
    br_fpi_var = ctk.BooleanVar(value=s.browser_first_party_isolation)
    br_strict_tp_var = ctk.BooleanVar(value=s.browser_strict_tracking_protection)
    br_tz_utc_var = ctk.BooleanVar(value=s.browser_timezone_utc)
    br_clear_var = ctk.BooleanVar(value=s.browser_clear_on_close)
    br_disposable_var = ctk.BooleanVar(value=s.browser_disposable_profile)
    br_kill_drop_var = ctk.BooleanVar(value=s.browser_kill_on_chain_drop)
    br_auto_relaunch_var = ctk.BooleanVar(value=s.browser_auto_relaunch)
    br_lock_profile_var = ctk.BooleanVar(value=s.browser_lock_managed_profile)

    def _br_toggle(parent: Any, text: str, var: ctk.BooleanVar, tip: str = "") -> ctk.CTkCheckBox:
        cb = ctk.CTkCheckBox(
            parent, text=text, variable=var, font=(FONT, 11),
            fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT,
        )
        cb.pack(anchor="w", padx=12, pady=5)
        return cb

    _br_toggle(
        browser_toggles_card, "Force browser proxy to local chain", br_force_proxy_var,
        "Forces Firefox traffic through Proxy God listener. Prevents direct bypass.",
    )
    _br_toggle(
        browser_toggles_card, "Disable WebRTC peer connections", br_disable_webrtc_var,
        "Stops common WebRTC local-IP leaks in browser calls.",
    )
    _br_toggle(
        browser_toggles_card, "Enable Resist Fingerprinting", br_rfp_var,
        "Turns on Firefox anti-fingerprinting mode (slightly changes site behavior).",
    )
    _br_toggle(
        browser_toggles_card, "Disable telemetry and studies", br_telemetry_var,
        "Disables browser diagnostics/study pings.",
    )
    _br_toggle(
        browser_toggles_card, "Enable First-Party Isolation", br_fpi_var,
        "Separates site storage per domain to reduce cross-site tracking.",
    )
    _br_toggle(
        browser_toggles_card, "Strict tracking protection", br_strict_tp_var,
        "Uses stronger tracker/cookie blocking in Firefox.",
    )
    _br_toggle(
        browser_toggles_card, "Timezone hardening (UTC-friendly timer precision)", br_tz_utc_var,
        "Reduces timing precision used in fingerprinting.",
    )
    _br_toggle(
        browser_toggles_card, "Clear cookies/cache/history on close", br_clear_var,
        "Wipes session traces from the managed profile when browser closes.",
    )
    _br_toggle(
        browser_toggles_card, "Disposable profile (delete on close)", br_disposable_var,
        "Deletes the entire managed profile folder on stop/close.",
    )
    _br_toggle(
        browser_toggles_card, "Kill browser if chain drops", br_kill_drop_var,
        "Stops browser immediately if chain becomes unhealthy.",
    )
    _br_toggle(
        browser_toggles_card, "Auto-relaunch browser while chain is healthy", br_auto_relaunch_var,
        "If browser exits unexpectedly, relaunch it automatically while chain stays up.",
    )
    _br_toggle(
        browser_toggles_card, "Lock to managed profile (recommended)", br_lock_profile_var,
        "Launches Firefox only with Proxy God profile and re-applies settings each launch.",
    )

    browser_action_row = ctk.CTkFrame(br_scroll, fg_color="transparent")
    browser_action_row.pack(fill="x", padx=8, pady=(2, 14))

    def _browser_cfg() -> BrowserConfig:
        return BrowserConfig(
            firefox_path=firefox_path_var.get().strip(),
            profile_dir=firefox_profile_var.get().strip(),
            clear_on_close=bool(br_clear_var.get()),
            disposable_profile=bool(br_disposable_var.get()),
            kill_on_chain_drop=bool(br_kill_drop_var.get()),
            auto_relaunch=bool(br_auto_relaunch_var.get()),
            lock_managed_profile=bool(br_lock_profile_var.get()),
            force_proxy=bool(br_force_proxy_var.get()),
            disable_webrtc=bool(br_disable_webrtc_var.get()),
            resist_fingerprinting=bool(br_rfp_var.get()),
            disable_telemetry=bool(br_telemetry_var.get()),
            first_party_isolation=bool(br_fpi_var.get()),
            strict_tracking_protection=bool(br_strict_tp_var.get()),
            timezone_utc=bool(br_tz_utc_var.get()),
            persona=_persona_key(),
            cookie_mode=_cookie_key(),
        )

    def _refresh_browser_status() -> None:
        if browser.is_running():
            browser_status_lbl.configure(
                text=f"Browser status: running (pid {browser.pid()})",
                text_color=GREEN,
            )
        else:
            if browser_should_run[0]:
                browser_status_lbl.configure(text="Browser status: waiting/restarting", text_color=YELLOW)
            else:
                browser_status_lbl.configure(text="Browser status: stopped", text_color=TEXT2)

    def _launch_browser_with_cfg(cfg: BrowserConfig) -> bool:
        if not svc.current_chain:
            _log("Chain not ready yet — starting chain and launching browser now.")
            _start()
        ok, msg = browser.launch(cfg, LISTEN_HOST, svc.settings.local_port)
        _log(msg)
        _refresh_browser_status()
        return bool(ok)

    def _launch_browser() -> None:
        _save_settings()
        cfg = _browser_cfg()
        browser_should_run[0] = True
        browser_relaunch_count[0] = 0  # user-initiated launch resets the cap
        if _launch_browser_with_cfg(cfg):
            browser_last_launch_ts[0] = time.monotonic()

    def _stop_browser() -> None:
        cfg = _browser_cfg()
        browser_should_run[0] = False
        browser_relaunch_count[0] = 0
        ok, msg = browser.stop(dispose=cfg.disposable_profile)
        _log(msg)
        _refresh_browser_status()
        if svc.sticky_remaining() > 0:
            svc.set_sticky(0)
            _log("Sticky exit released (browser stopped).")

    launch_browser_btn = _btn(
        browser_action_row, "Launch Hardened Firefox", _launch_browser, w=190, h=34, font=(FONT, 12)
    )
    launch_browser_btn.pack(side="left", padx=(0, 8))
    stop_browser_btn = _btn(
        browser_action_row, "Stop Browser", _stop_browser, w=110, h=34,
        fg_color="#7f1d1d", hover_color="#991b1b",
    )
    stop_browser_btn.pack(side="left")

    # ─── TAB: BAN TESTER ───────────────────────────────────────────────────────
    ban_scroll = ctk.CTkScrollableFrame(tab_ban, fg_color=BG, scrollbar_button_color=ACCENT)
    ban_scroll.pack(fill="both", expand=True, padx=4, pady=4)

    ban_head = ctk.CTkFrame(ban_scroll, fg_color=CARD, corner_radius=10, border_width=1, border_color=GLOW)
    ban_head.pack(fill="x", padx=8, pady=(6, 8))
    ctk.CTkLabel(
        ban_head, text="Ban / Reachability Tester",
        font=(FONT, 13, "bold"), text_color=CYAN,
    ).pack(anchor="w", padx=12, pady=(10, 2))
    ctk.CTkLabel(
        ban_head,
        text=(
            "Tests social, shopping, crypto exchanges, and DNS providers through your ACTIVE chain. "
            "Detects exit-IP bans and geo-blocks."
        ),
        font=(FONT, 10), text_color=TEXT2, wraplength=880, justify="left",
    ).pack(anchor="w", padx=12, pady=(0, 6))

    # Category selector
    ban_cat_row = ctk.CTkFrame(ban_head, fg_color="transparent")
    ban_cat_row.pack(fill="x", padx=12, pady=(0, 6))
    ctk.CTkLabel(ban_cat_row, text="Category:", font=(FONT, 10), text_color=TEXT2, width=70,
                 anchor="w").pack(side="left")
    _ban_cat_keys   = list(SITE_CATEGORIES.keys())   # "Social / General", "Shopping", etc.
    ban_cat_var     = ctk.StringVar(value="All")
    ban_cat_menu    = ctk.CTkOptionMenu(
        ban_cat_row, values=_ban_cat_keys, variable=ban_cat_var,
        fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT,
        text_color=TEXT, width=200,
    )
    ban_cat_menu.pack(side="left", padx=(0, 8))

    ban_status_var = ctk.StringVar(value="Status: idle")
    ctk.CTkLabel(ban_head, textvariable=ban_status_var, font=(FONT, 11, "bold"), text_color=TEXT).pack(
        anchor="w", padx=12, pady=(0, 6)
    )

    ban_results_frame = ctk.CTkFrame(
        ban_scroll, fg_color=CARD, corner_radius=10, border_width=1, border_color=GLOW,
    )
    ban_results_frame.pack(fill="both", expand=True, padx=8, pady=(0, 8))

    ban_rows_wrap = ctk.CTkFrame(ban_results_frame, fg_color="transparent")
    ban_rows_wrap.pack(fill="both", expand=True, padx=8, pady=8)

    def _ban_row(site: str, status: str, detail: str, color: str, cat_header: str = "") -> None:
        if cat_header:
            ctk.CTkLabel(
                ban_rows_wrap, text=f"  {cat_header}",
                font=(FONT, 10, "bold"), text_color=CYAN, anchor="w",
            ).pack(fill="x", padx=4, pady=(6, 1))
        row = ctk.CTkFrame(ban_rows_wrap, fg_color=PANEL, corner_radius=6, height=30)
        row.pack(fill="x", pady=2)
        row.pack_propagate(False)
        ctk.CTkLabel(row, text=site, font=(FONT, 11, "bold"), text_color=TEXT,
                     width=160, anchor="w").pack(side="left", padx=(8, 4))
        ctk.CTkLabel(row, text=status, font=(FONT, 10, "bold"), text_color=color,
                     width=72).pack(side="left", padx=4)
        ctk.CTkLabel(row, text=detail, font=("Consolas", 10), text_color=TEXT2,
                     anchor="w").pack(side="left", fill="x", expand=True, padx=(6, 8))

    def _render_ban_results(results: list[BanTestResult]) -> None:
        for child in list(ban_rows_wrap.winfo_children()):
            child.destroy()
        if not results:
            ctk.CTkLabel(ban_rows_wrap, text="No results yet.", font=(FONT, 11),
                         text_color=TEXT2).pack(anchor="w", padx=8, pady=8)
            return
        seen_cats: set[str] = set()
        for r in results:
            cat = r.category or ""
            header = cat if cat and cat not in seen_cats else ""
            if cat:
                seen_cats.add(cat)
            if r.status == "ok":
                st, color = "OK", GREEN
            elif r.status == "banned":
                st, color = "BANNED", ORANGE
            else:
                st, color = "ERROR", RED
            _ban_row(r.site, st, r.detail, color, cat_header=header)

    _render_ban_results([])

    ban_action = ctk.CTkFrame(ban_head, fg_color="transparent")
    ban_action.pack(fill="x", padx=12, pady=(0, 10))

    def _start_ban_test() -> None:
        if not service_running[0] or not svc.current_chain:
            ban_status_var.set("Status: chain not running")
            _log("Ban tester: start the chain first, then run tests.")
            return
        proxy_url = f"http://{svc.settings.listen_addr()}"
        chosen_cat = ban_cat_var.get()
        # "All" means every category
        cats = None if chosen_cat == "All" else [chosen_cat]
        ban_status_var.set("Status: running…")
        ban_start_btn.configure(state="disabled")
        _render_ban_results([])

        def work() -> None:
            timeout = min(18.0, max(8.0, float(svc.settings.validation_timeout_seconds) + 3.0))
            results = run_ban_tests(proxy_url, timeout_seconds=timeout, categories=cats)
            ok_n     = sum(1 for r in results if r.status == "ok")
            banned_n = sum(1 for r in results if r.status == "banned")
            err_n    = len(results) - ok_n - banned_n

            def done() -> None:
                _render_ban_results(results)
                ban_status_var.set(f"Status: done — ok={ok_n}  banned={banned_n}  errors={err_n}")
                _log(f"Ban tester done via {proxy_url} — ok={ok_n}, banned={banned_n}, errors={err_n}.")
                ban_start_btn.configure(state="normal")

            root.after(0, done)

        threading.Thread(target=work, daemon=True).start()

    ban_start_btn = _btn(
        ban_action, "Start test", _start_ban_test,
        w=120, h=30, fg_color=ACCENT2, hover_color=ACCENT,
    )
    ban_start_btn.pack(side="left")

    # ─── TAB: SIGNUP PREP ──────────────────────────────────────────────────────
    signup_scroll = ctk.CTkScrollableFrame(tab_signup, fg_color=BG, scrollbar_button_color=ACCENT)
    signup_scroll.pack(fill="both", expand=True, padx=4, pady=4)

    signup_draft = load_draft()

    def _signup_card(title: str, subtitle: str = "") -> ctk.CTkFrame:
        ctk.CTkLabel(signup_scroll, text=title, font=(FONT, 13, "bold"), text_color=CYAN).pack(
            anchor="w", padx=8, pady=(12, 0)
        )
        if subtitle:
            ctk.CTkLabel(
                signup_scroll, text=subtitle, font=(FONT, 10), text_color=TEXT2,
                wraplength=900, justify="left",
            ).pack(anchor="w", padx=8, pady=(0, 4))
        f = ctk.CTkFrame(signup_scroll, fg_color=CARD, corner_radius=10, border_width=1, border_color=GLOW)
        f.pack(fill="x", padx=8, pady=(4, 8))
        return f

    signup_head = _signup_card(
        "Signup autofill (manual submit)",
        "Opens signup pages through your chain and autofills your fields. You solve captchas and click submit.",
    )

    site_row = ctk.CTkFrame(signup_head, fg_color="transparent")
    site_row.pack(fill="x", padx=12, pady=(10, 4))
    ctk.CTkLabel(site_row, text="Site", width=70, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left")
    signup_site_keys = list(SIGNUP_PRESET_KEYS) + ["custom"]
    signup_site_labels = [
        (SIGNUP_PRESETS[k].label if k in SIGNUP_PRESETS else "Custom URL")
        for k in signup_site_keys
    ]
    signup_site_var = ctk.StringVar(
        value=next(
            (signup_site_labels[i] for i, k in enumerate(signup_site_keys) if k == signup_draft.site_key),
            signup_site_labels[0],
        )
    )
    custom_url_row = ctk.CTkFrame(signup_head, fg_color="transparent")
    custom_url_row.pack(fill="x", padx=12, pady=(0, 4))
    ctk.CTkLabel(custom_url_row, text="URL", width=70, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left")
    signup_custom_url = ctk.CTkEntry(custom_url_row, height=28, fg_color=BG, border_color=ACCENT)
    signup_custom_url.pack(side="left", fill="x", expand=True)
    signup_custom_url.insert(0, signup_draft.custom_url or "")

    def _signup_site_key() -> str:
        label = signup_site_var.get()
        for i, lbl in enumerate(signup_site_labels):
            if lbl == label:
                return signup_site_keys[i]
        return signup_site_keys[0]

    def _on_signup_site_change(_choice: str = "") -> None:
        key = _signup_site_key()
        preset = SIGNUP_PRESETS.get(key)
        if preset and preset.url:
            signup_custom_url.delete(0, "end")
            signup_custom_url.insert(0, preset.url)

    signup_site_menu = ctk.CTkOptionMenu(
        site_row, values=signup_site_labels, variable=signup_site_var,
        fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT, text_color=TEXT, width=260,
        command=_on_signup_site_change,
    )
    signup_site_menu.pack(side="left", padx=(0, 8))

    # Populate URL field for the initially-selected site
    _on_signup_site_change()

    def _signup_field(parent: Any, label: str, show: str = "") -> ctk.CTkEntry:
        row = ctk.CTkFrame(parent, fg_color="transparent")
        row.pack(fill="x", padx=12, pady=3)
        ctk.CTkLabel(row, text=label, width=70, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left")
        e = ctk.CTkEntry(row, height=28, fg_color=BG, border_color=ACCENT, show=show)
        e.pack(side="left", fill="x", expand=True)
        return e

    signup_email = _signup_field(signup_head, "Email")
    signup_email.insert(0, signup_draft.email or "")
    signup_pass = _signup_field(signup_head, "Password", show="*")
    signup_pass.insert(0, signup_draft.password or "")
    signup_user = _signup_field(signup_head, "Username")
    signup_user.insert(0, signup_draft.username or "")
    signup_fname = _signup_field(signup_head, "First name")
    signup_fname.insert(0, signup_draft.first_name or "")
    signup_lname = _signup_field(signup_head, "Last name")
    signup_lname.insert(0, signup_draft.last_name or "")
    signup_notes = _signup_field(signup_head, "Notes")
    signup_notes.insert(0, signup_draft.notes or "")

    def _clipboard_copy(text: str, label: str) -> None:
        t = (text or "").strip()
        if not t:
            _log(f"Signup prep: nothing to copy for {label}.")
            return
        root.clipboard_clear()
        root.clipboard_append(t)
        root.update()
        _log(f"Copied {label} to clipboard.")

    def _collect_signup_draft() -> SignupDraft:
        return SignupDraft(
            site_key=_signup_site_key(),
            custom_url=signup_custom_url.get().strip(),
            email=signup_email.get().strip(),
            password=signup_pass.get().strip(),
            username=signup_user.get().strip(),
            first_name=signup_fname.get().strip(),
            last_name=signup_lname.get().strip(),
            notes=signup_notes.get().strip(),
        )

    def _persist_signup_draft() -> SignupDraft:
        d = _collect_signup_draft()
        save_draft(d)
        return d

    signup_btn_row = ctk.CTkFrame(signup_head, fg_color="transparent")
    signup_btn_row.pack(fill="x", padx=12, pady=(8, 6))

    def _gen_signup_password() -> None:
        pw = generate_password()
        signup_pass.delete(0, "end")
        signup_pass.insert(0, pw)
        _persist_signup_draft()
        _log("Generated signup password.")

    def _gen_signup_names() -> None:
        f, l = random_full_name()
        signup_fname.delete(0, "end"); signup_fname.insert(0, f)
        signup_lname.delete(0, "end"); signup_lname.insert(0, l)
        _persist_signup_draft()
        _log(f"Generated name: {f} {l}")

    def _gen_signup_username() -> None:
        f = signup_fname.get().strip() or random_first_name()
        l = signup_lname.get().strip() or random_last_name()
        u = random_username(f, l)
        signup_user.delete(0, "end"); signup_user.insert(0, u)
        _persist_signup_draft()
        _log(f"Generated username: {u}")

    def _gen_signup_everything() -> None:
        f, l = random_full_name()
        signup_fname.delete(0, "end"); signup_fname.insert(0, f)
        signup_lname.delete(0, "end"); signup_lname.insert(0, l)
        signup_user.delete(0, "end"); signup_user.insert(0, random_username(f, l))
        signup_pass.delete(0, "end"); signup_pass.insert(0, generate_password())
        _persist_signup_draft()
        _log(f"Generated identity: {f} {l}")

    _btn(signup_btn_row, "Gen password", _gen_signup_password, w=100, h=28,
         fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6))
    _btn(signup_btn_row, "Gen name", _gen_signup_names, w=80, h=28,
         fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6))
    _btn(signup_btn_row, "Gen username", _gen_signup_username, w=100, h=28,
         fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6))
    _btn(signup_btn_row, "Gen ALL", _gen_signup_everything, w=80, h=28,
         fg_color=ACCENT2, hover_color=ACCENT).pack(side="left", padx=(0, 12))
    _btn(signup_btn_row, "Copy email", lambda: _clipboard_copy(signup_email.get(), "email"),
         w=88, h=28, fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6))
    _btn(signup_btn_row, "Copy pass", lambda: _clipboard_copy(signup_pass.get(), "password"),
         w=88, h=28, fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6))

    # Sticky exit + paranoid mode toggles
    signup_opts_row = ctk.CTkFrame(signup_head, fg_color="transparent")
    signup_opts_row.pack(fill="x", padx=12, pady=(0, 6))
    sticky_exit_var = ctk.BooleanVar(value=True)
    paranoid_signup_var = ctk.BooleanVar(value=False)
    ctk.CTkCheckBox(
        signup_opts_row, text="Sticky exit (hold final hop while signup is open)",
        variable=sticky_exit_var, font=(FONT, 10),
        fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT,
    ).pack(side="left", padx=(0, 12))
    ctk.CTkCheckBox(
        signup_opts_row, text="Paranoid mode (disposable profile + DNS flush)",
        variable=paranoid_signup_var, font=(FONT, 10),
        fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT,
    ).pack(side="left")

    # ── Pre-flight checklist ─────────────────────────────────────────────────
    preflight_card = _signup_card(
        "Pre-flight check",
        "Exhaustive readiness check: chain, exit IP, geo/ASN, DNS leak, WebRTC, "
        "IPv6, HTTPS tunnel, target site reachability, and direct-IP comparison.",
    )
    pf_rows: dict[str, dict[str, ctk.StringVar | ctk.CTkLabel]] = {}

    _PF_CHECKS = [
        ("chain",       "Chain running"),
        ("exit_ip",     "Exit IP (via chain)"),
        ("direct_ip",   "Direct IP (no chain)"),
        ("leak_compare","IP leak check"),
        ("intel",       "Geo / ASN intel"),
        ("datacenter",  "Residential ASN"),
        ("dns",         "DNS routing"),
        ("ipv6",        "IPv6 binding"),
        ("webrtc",      "WebRTC policy"),
        ("https_tunnel","HTTPS tunnel (CONNECT)"),
        ("target",      "Target site reachable"),
        ("firefox",     "Firefox path"),
    ]

    def _pf_row(key: str, label: str) -> None:
        row = ctk.CTkFrame(preflight_card, fg_color=PANEL, corner_radius=6)
        row.pack(fill="x", padx=12, pady=2)
        dot = ctk.CTkLabel(row, text="●", font=(FONT, 14), text_color=DIM, width=18)
        dot.pack(side="left", padx=(8, 4))
        ctk.CTkLabel(row, text=label, font=(FONT, 11, "bold"), text_color=TEXT,
                     width=180, anchor="w").pack(side="left")
        var = ctk.StringVar(value="not run")
        ctk.CTkLabel(row, textvariable=var, font=(FONT, 10), text_color=TEXT2,
                     anchor="w").pack(side="left", fill="x", expand=True, padx=(6, 8), pady=4)
        pf_rows[key] = {"var": var, "dot": dot}

    for _k, _lbl in _PF_CHECKS:
        _pf_row(_k, _lbl)

    def _pf_set(key: str, status: str, detail: str) -> None:
        r = pf_rows.get(key)
        if not r:
            return
        color = {"ok": GREEN, "warn": ORANGE, "fail": RED, "running": YELLOW}.get(status, DIM)
        r["dot"].configure(text_color=color)  # type: ignore[union-attr]
        r["var"].set(detail)                  # type: ignore[union-attr]

    pf_summary_var = ctk.StringVar(value="Not run.")
    ctk.CTkLabel(preflight_card, textvariable=pf_summary_var, font=(FONT, 10, "bold"),
                 text_color=TEXT).pack(anchor="w", padx=12, pady=(6, 8))

    pf_running = [False]

    def _run_preflight() -> None:
        if pf_running[0]:
            return
        draft = _persist_signup_draft()
        url = resolve_signup_url(draft)
        if not url:
            pf_summary_var.set("Pick a site or enter a custom URL first.")
            return
        pf_running[0] = True
        for k, _ in _PF_CHECKS:
            _pf_set(k, "running", "checking…")
        pf_summary_var.set("Running pre-flight…")

        # Immediate sync checks ──────────────────────────────────────────────
        chain_ok = service_running[0] and bool(svc.current_chain)
        if chain_ok:
            _pf_set("chain", "ok", f"{len(svc.current_chain)} hop(s) active")
        else:
            _pf_set("chain", "fail", "chain not running — start first")

        # Firefox path check
        ff_path = firefox_path_var.get().strip() or default_firefox_path()
        from pathlib import Path as _Path
        if ff_path and _Path(ff_path).is_file():
            _pf_set("firefox", "ok", ff_path)
        else:
            _pf_set("firefox", "fail", f"not found: {ff_path or '(no path set)'}")

        proxy_url = f"http://{svc.settings.listen_addr()}"
        target_host = urlparse(url).hostname or "(unknown)"

        def work() -> None:
            from .leak_detect import is_chain_leak, leak_reason
            from .validator import get_direct_ip as _get_direct_ip
            import asyncio as _asyncio

            timeout = min(15.0, max(8.0, float(svc.settings.validation_timeout_seconds) + 2.0))
            warns: list[str] = []
            vpn_active = detect_vpn().active

            # Single dedicated event loop for the whole preflight pass.  Using
            # `asyncio.run()` per call tore down the loop each time, which
            # caused nested-loop errors when caller context (e.g. async http
            # libraries) leaked tasks across invocations.
            _loop = _asyncio.new_event_loop()
            _asyncio.set_event_loop(_loop)

            def _run_async(coro):
                return _loop.run_until_complete(coro)

            # ── 1. Direct IP (bypass chain) ──────────────────────────────────
            try:
                direct_ip = _run_async(_get_direct_ip(svc.settings.ip_check_url, timeout / 2))
            except Exception:
                direct_ip = None

            def _set_direct(ip: str | None) -> None:
                if ip:
                    _pf_set("direct_ip", "ok", ip)
                else:
                    _pf_set("direct_ip", "warn", "could not reach check URL directly")

            root.after(0, lambda ip=direct_ip: _set_direct(ip))

            # ── 2. Exit IP via chain ─────────────────────────────────────────
            if chain_ok:
                intel = fetch_exit_intel(proxy_url, timeout_seconds=timeout)
                exit_ip_str = intel.ip if intel.ok else (v_exit_ip.get() or "").strip()

                def _set_exit(ip_: str, intel_: Any) -> None:
                    if ip_ and ip_ not in ("—", ""):
                        _pf_set("exit_ip", "ok", ip_)
                    else:
                        _pf_set("exit_ip", "warn", "no exit IP yet")
                    # IP leak compare (same logic as production leak_detect)
                    if direct_ip and ip_ and ip_ not in ("—", ""):
                        if is_chain_leak(ip_, direct_ip, vpn_active):
                            reason = leak_reason(ip_, direct_ip, vpn_active)
                            _pf_set("leak_compare", "fail", reason[:80])
                            warns.append(f"IP leak: {reason}")
                        else:
                            _pf_set("leak_compare", "ok",
                                    f"exit {ip_} ≠ direct {direct_ip} ✓")
                    else:
                        _pf_set("leak_compare", "warn", "cannot compare — missing IPs")
                    # Geo/ASN
                    if intel_.ok:
                        _apply_intel(intel_)
                        loc = ", ".join([b for b in (intel_.city, intel_.country) if b]) or "—"
                        asn_str = f"AS{intel_.asn}" if intel_.asn else ""
                        if intel_.is_proxy_flagged:
                            _pf_set("intel", "fail", f"proxy-flagged · {intel_.org or intel_.isp}")
                            warns.append("exit IP is proxy-flagged")
                        elif intel_.is_datacenter:
                            _pf_set("intel", "warn", f"datacenter · {loc} {asn_str}")
                            _pf_set("datacenter", "warn", f"DC ASN — {intel_.org or intel_.isp}")
                            warns.append("exit IP is datacenter ASN")
                        else:
                            _pf_set("intel", "ok", f"{loc} {asn_str}")
                            _pf_set("datacenter", "ok", f"residential/ISP — {intel_.org or intel_.isp}")
                    else:
                        _pf_set("intel", "warn", intel_.detail or "lookup failed")
                        _pf_set("datacenter", "warn", "intel unavailable")

                root.after(0, lambda i=exit_ip_str, d=intel: _set_exit(i, d))
            else:
                for k in ("exit_ip", "leak_compare", "intel", "datacenter"):
                    root.after(0, lambda k_=k: _pf_set(k_, "fail", "chain not running"))

            # ── 3. DNS routing check ─────────────────────────────────────────
            try:
                dns_rep = run_dns_leak_test(
                    proxy_url=proxy_url if chain_ok else None,
                    timeout=min(10.0, timeout),
                )
                def _set_dns(r: Any) -> None:
                    if r.leaked:
                        _pf_set("dns", "fail", r.summary[:80])
                        warns.append("DNS leak detected")
                    elif r.has_external_resolver:
                        _pf_set("dns", "warn", r.summary[:80])
                    else:
                        _pf_set("dns", "ok", r.summary[:80])
                root.after(0, lambda r_=dns_rep: _set_dns(r_))
            except Exception as e:
                root.after(0, lambda: _pf_set("dns", "warn", f"check error: {e}"))

            # ── 4. IPv6 binding ──────────────────────────────────────────────
            try:
                from .leak_audit import _check_ipv6_active
                v6_on, v6_msg = _check_ipv6_active()
                def _set_v6(on_: bool, msg_: str) -> None:
                    if on_:
                        _pf_set("ipv6", "warn", f"IPv6 active: {msg_}")
                        warns.append("IPv6 enabled (possible leak path)")
                    else:
                        _pf_set("ipv6", "ok", msg_)
                root.after(0, lambda on=v6_on, msg=v6_msg: _set_v6(on, msg))
            except Exception as e:
                root.after(0, lambda: _pf_set("ipv6", "warn", str(e)))

            # ── 5. WebRTC ────────────────────────────────────────────────────
            try:
                profile = _Path(firefox_profile_var.get().strip() or default_profile_dir())
                rtc_rep = run_webrtc_check(profile_dir=profile)
                def _set_rtc(r: Any) -> None:
                    if r.any_leak_risk:
                        issues_short = "; ".join(r.issues[:2])[:70]
                        _pf_set("webrtc", "warn", issues_short)
                    else:
                        _pf_set("webrtc", "ok", "all surfaces protected")
                root.after(0, lambda r_=rtc_rep: _set_rtc(r_))
            except Exception as e:
                root.after(0, lambda: _pf_set("webrtc", "warn", str(e)))

            # ── 6. HTTPS tunnel probe ─────────────────────────────────────────
            if chain_ok:
                try:
                    from .validator import check_https_tunnel
                    tun_ok, tun_msg = _run_async(
                        check_https_tunnel(proxy_url, timeout)
                    )
                    def _set_tun(ok_: bool, msg_: str) -> None:
                        if ok_:
                            _pf_set("https_tunnel", "ok", msg_)
                        else:
                            _pf_set("https_tunnel", "fail", msg_)
                            warns.append("HTTPS CONNECT tunnel broken")
                    root.after(0, lambda ok=tun_ok, msg=tun_msg: _set_tun(ok, msg))
                except Exception as e:
                    root.after(0, lambda: _pf_set("https_tunnel", "warn", str(e)))
            else:
                root.after(0, lambda: _pf_set("https_tunnel", "fail", "chain not running"))

            # ── 7. Target site reachability ───────────────────────────────────
            if chain_ok:
                site_res = test_single_site(proxy_url, target_host, url, timeout_seconds=timeout)
                def _set_site(r: Any) -> None:
                    if r.status == "ok":
                        _pf_set("target", "ok", f"{target_host} — {r.detail}")
                    elif r.status == "banned":
                        _pf_set("target", "fail", f"likely BANNED · {r.detail}")
                        warns.append("target site likely blocks this exit IP")
                    else:
                        _pf_set("target", "warn", r.detail)
                root.after(0, lambda r_=site_res: _set_site(r_))
            else:
                root.after(0, lambda: _pf_set("target", "fail", "chain not running"))

            # ── Final summary ─────────────────────────────────────────────────
            # Schedule _final via root.after(0, ...) so it is enqueued AFTER
            # all previously-scheduled root.after(0, ...) callbacks (which
            # populate `warns`).  A fixed 200 ms delay could fire before those
            # callbacks execute on a slow main thread.
            def _final() -> None:
                pf_running[0] = False
                if warns:
                    pf_summary_var.set(f"⚠ {len(warns)} warning(s): " + " · ".join(warns[:3]))
                else:
                    pf_summary_var.set("✓ All pre-flight checks passed — good to go.")

            root.after(0, _final)

            # Tear down the dedicated event loop now that all coroutines are
            # done; leaving it open would leak the underlying selector socket.
            try:
                _loop.close()
            except Exception:  # noqa: BLE001
                pass

        threading.Thread(target=work, daemon=True).start()

    pf_btn_row = ctk.CTkFrame(preflight_card, fg_color="transparent")
    pf_btn_row.pack(fill="x", padx=12, pady=(0, 10))
    _btn(pf_btn_row, "Run pre-flight", _run_preflight, w=140, h=28,
         fg_color=ACCENT2, hover_color=ACCENT).pack(side="left")

    signup_status_var = ctk.StringVar(value="Ready — start chain, then Open & Autofill.")
    ctk.CTkLabel(signup_head, textvariable=signup_status_var, font=(FONT, 10), text_color=TEXT2).pack(
        anchor="w", padx=12, pady=(0, 8)
    )

    signup_action = ctk.CTkFrame(signup_scroll, fg_color="transparent")
    signup_action.pack(fill="x", padx=8, pady=(0, 6))
    signup_launch_retries = [0]

    accounts_card = _signup_card("Saved accounts", "Logged locally with exit IP + hop used at save time.")

    accounts_list = ctk.CTkScrollableFrame(
        accounts_card, fg_color=BG, height=180, corner_radius=6, scrollbar_button_color=ACCENT,
    )
    accounts_list.pack(fill="x", padx=12, pady=(8, 10))

    def _refresh_accounts_ui() -> None:
        for child in list(accounts_list.winfo_children()):
            child.destroy()
        rows = load_accounts()
        if not rows:
            ctk.CTkLabel(accounts_list, text="No saved accounts yet.", font=(FONT, 10),
                         text_color=TEXT2).pack(anchor="w", padx=8, pady=8)
            return
        for rec in rows[:60]:
            row_frame = ctk.CTkFrame(accounts_list, fg_color=PANEL, corner_radius=6)
            row_frame.pack(fill="x", pady=2, padx=4)

            info_lbl = ctk.CTkLabel(
                row_frame,
                text=f"{rec.created_at[:16]}  {rec.site}  {rec.email or rec.username or '—'}  exit={rec.exit_ip or '—'}",
                font=("Consolas", 10), text_color=TEXT, anchor="w",
            )
            info_lbl.pack(side="left", fill="x", expand=True, padx=(8, 4), pady=4)

            def _copy_email(r: Any = rec) -> None:
                v = r.email or r.username or ""
                root.clipboard_clear()
                root.clipboard_append(v)
                _log(f"Copied email/user: {v}")

            def _copy_pass(r: Any = rec) -> None:
                root.clipboard_clear()
                root.clipboard_append(r.password or "")
                _log(f"Copied password for {r.email or r.username or r.site}.")

            ctk.CTkButton(
                row_frame, text="Copy Email", width=80, height=22,
                font=(FONT, 10), fg_color=ACCENT2, hover_color=ACCENT,
                command=_copy_email,
            ).pack(side="right", padx=(2, 4), pady=4)
            ctk.CTkButton(
                row_frame, text="Copy Pass", width=76, height=22,
                font=(FONT, 10), fg_color="#1e3a1e", hover_color="#2e5a2e",
                text_color=GREEN,
                command=_copy_pass,
            ).pack(side="right", padx=(2, 2), pady=4)

    def _current_exit_meta() -> tuple[str, str]:
        ip = (v_exit_ip.get() or "—").strip()
        if ip == "—":
            ip = ""
        hop = ""
        chain = svc.current_chain
        if chain:
            hop = redact_proxy_url(chain[-1])
        return ip, hop

    def _launch_signup_autofill() -> None:
        draft = _persist_signup_draft()
        url = resolve_signup_url(draft)
        if not url:
            signup_status_var.set("Missing signup URL.")
            _log("Signup prep: pick a site or enter a custom URL.")
            return
        if not draft.email and not draft.username:
            signup_status_var.set("Enter email or username.")
            _log("Signup prep: email or username required for autofill.")
            return
        if not draft.password:
            signup_status_var.set("Enter or generate a password.")
            _log("Signup prep: password required for autofill.")
            return

        cfg = _browser_cfg()
        cfg.lock_managed_profile = True
        paranoid = bool(paranoid_signup_var.get())
        if paranoid:
            cfg.disposable_profile = True
            cfg.clear_on_close = True
            cfg.kill_on_chain_drop = True
            ok_dns, msg_dns = flush_dns_cache()
            _log(f"Paranoid mode: DNS flush — {msg_dns}" if ok_dns else f"Paranoid mode: DNS flush failed — {msg_dns}")
        if not service_running[0] or not svc.current_chain:
            if signup_launch_retries[0] == 0:
                _log("Signup prep: starting chain first…")
                _start()
            signup_launch_retries[0] += 1
            if signup_launch_retries[0] > 12:
                signup_status_var.set("Chain did not start in time.")
                signup_launch_retries[0] = 0
                _log("Signup prep: chain not ready — start chain manually.")
                return
            signup_status_var.set("Waiting for chain…")
            root.after(2500, _launch_signup_autofill)
            return
        signup_launch_retries[0] = 0

        if bool(sticky_exit_var.get()):
            svc.set_sticky(1800)
            _log("Sticky exit engaged — auto-rotation suspended for 30 min.")

        ok, msg = browser.launch(
            cfg, LISTEN_HOST, svc.settings.local_port,
            start_url=url, signup_draft=draft,
        )
        _log(msg)
        _refresh_browser_status()
        if ok:
            preset = SIGNUP_PRESETS.get(draft.site_key)
            label = preset.label if preset else "Custom"
            extras = []
            if bool(sticky_exit_var.get()):
                extras.append("sticky")
            if paranoid:
                extras.append("paranoid")
            tag = f" [{' · '.join(extras)}]" if extras else ""
            signup_status_var.set(f"Opened {label}{tag} — autofill active (you submit + captcha).")
            _log(f"Signup prep: opened {url} with autofill extension{tag}.")
            # Record draft metadata only — password saved when user clicks Save Account
            _save_signup_account_draft_only()
        else:
            signup_status_var.set("Browser launch failed.")
            svc.set_sticky(0)

    def _save_signup_account_draft_only() -> None:
        """After browser open: save email/exit metadata without password until user confirms."""
        draft = _persist_signup_draft()
        url = resolve_signup_url(draft)
        exit_ip, exit_hop = _current_exit_meta()
        preset = SIGNUP_PRESETS.get(draft.site_key)
        site_label = preset.label if preset else "Custom"
        rec = AccountRecord(
            id=datetime.now().strftime("%Y%m%d%H%M%S%f"),
            site=site_label,
            url=url,
            email=draft.email,
            password="",
            username=draft.username,
            first_name=draft.first_name,
            last_name=draft.last_name,
            exit_ip=exit_ip,
            exit_hop=exit_hop,
            notes=draft.notes,
            status="pending",
            created_at=datetime.now().isoformat(timespec="seconds"),
        )
        append_account(rec)
        _refresh_accounts_ui()
        _log(f"Signup prep: draft account {draft.email or draft.username} ({site_label}) — save password after signup.")

    def _save_signup_account() -> None:
        draft = _persist_signup_draft()
        url = resolve_signup_url(draft)
        exit_ip, exit_hop = _current_exit_meta()
        preset = SIGNUP_PRESETS.get(draft.site_key)
        site_label = preset.label if preset else "Custom"
        rec = AccountRecord(
            id=datetime.now().strftime("%Y%m%d%H%M%S%f"),
            site=site_label,
            url=url,
            email=draft.email,
            password=draft.password,
            username=draft.username,
            first_name=draft.first_name,
            last_name=draft.last_name,
            exit_ip=exit_ip,
            exit_hop=exit_hop,
            notes=draft.notes,
            status="created",
            created_at=datetime.now().isoformat(timespec="seconds"),
        )
        append_account(rec)
        _refresh_accounts_ui()
        signup_status_var.set(f"Saved account for {site_label}.")
        _log(f"Signup prep: saved account {draft.email or draft.username} ({site_label}).")

    def _preflight_then_open() -> None:
        _run_preflight()

        def follow_up(attempts: int = 0) -> None:
            if pf_running[0] and attempts < 30:
                root.after(500, lambda: follow_up(attempts + 1))
                return
            _launch_signup_autofill()

        root.after(500, follow_up)

    signup_open_btn = _btn(
        signup_action, "Open & Autofill", _launch_signup_autofill,
        w=140, h=34, fg_color=ACCENT2, hover_color=ACCENT, font=(FONT, 12),
    )
    signup_open_btn.pack(side="left", padx=(0, 8))
    _btn(
        signup_action, "Pre-flight + Open", _preflight_then_open,
        w=150, h=34, fg_color=ACCENT, hover_color=ACCENT2,
    ).pack(side="left", padx=(0, 8))
    _btn(
        signup_action, "Save account", _save_signup_account,
        w=110, h=34, fg_color=GREEN, hover_color="#00b377",
    ).pack(side="left", padx=(0, 8))
    _btn(
        signup_action, "Refresh list", _refresh_accounts_ui,
        w=100, h=34, fg_color=DIM, hover_color=ACCENT,
    ).pack(side="left")

    _refresh_accounts_ui()

    # ─── TAB: PRIVACY ──────────────────────────────────────────────────────────
    priv_scroll = ctk.CTkScrollableFrame(tab_privacy, fg_color=BG,
                                         scrollbar_button_color=ACCENT)
    priv_scroll.pack(fill="both", expand=True, padx=4, pady=4)

    def _priv_section(title: str, subtitle: str = "") -> ctk.CTkFrame:
        ctk.CTkLabel(priv_scroll, text=title, font=(FONT, 13, "bold"),
                     text_color=CYAN).pack(anchor="w", padx=8, pady=(12, 0))
        if subtitle:
            ctk.CTkLabel(priv_scroll, text=subtitle, font=(FONT, 10),
                         text_color=TEXT2, wraplength=900, justify="left").pack(
                anchor="w", padx=8, pady=(0, 4))
        f = ctk.CTkFrame(
            priv_scroll, fg_color=CARD, corner_radius=10,
            border_width=1, border_color=GLOW,
        )
        f.pack(fill="x", padx=8, pady=(4, 8))
        return f

    status_card = _priv_section(
        "Network identity",
        "VPN detection drives leak checks: strict without VPN, subnet-aware with VPN.",
    )
    status_row = ctk.CTkFrame(status_card, fg_color="transparent")
    status_row.pack(fill="x", padx=12, pady=10)
    vpn_status_lbl = ctk.CTkLabel(status_row, text="VPN: scanning…",
                                  font=(FONT, 12, "bold"), text_color=TEXT)
    vpn_status_lbl.pack(anchor="w")
    leak_mode_lbl = ctk.CTkLabel(status_row, text="Leak check: —",
                                 font=(FONT, 11), text_color=TEXT2)
    leak_mode_lbl.pack(anchor="w", pady=(4, 0))

    def _refresh_vpn_ui() -> None:
        vs = detect_vpn()
        nonlocal outer_hop_label
        if vs.active:
            outer_hop_label = vs.short_label()[:12]
            vpn_status_lbl.configure(
                text=f"VPN: {vs.label}" + (f"  ({vs.adapter})" if vs.adapter else ""),
                text_color=GREEN,
            )
            leak_mode_lbl.configure(text="Leak check: VPN-aware (/16 subnet)")
        else:
            outer_hop_label = "Direct"
            vpn_status_lbl.configure(text="VPN: not detected (direct connection)",
                                     text_color=YELLOW)
            leak_mode_lbl.configure(text="Leak check: strict (exit must differ from your IP)")

    _btn(status_card, "Refresh VPN status", _refresh_vpn_ui, w=140, h=28).pack(
        anchor="w", padx=12, pady=(0, 10))

    toggles_card = _priv_section(
        "Protections while chain is running",
        "Applied on Start, restored on Stop. Admin required for MAC, hostname, IPv6, and WebRTC.",
    )
    mac_var = ctk.BooleanVar(value=s.mac_spoof_enabled)
    mac_rotate_var = ctk.BooleanVar(value=s.mac_rotate_on_chain_rotate)
    host_var = ctk.BooleanVar(value=s.spoof_hostname_enabled)
    dns_flush_var = ctk.BooleanVar(value=s.flush_dns_on_rotate)
    ipv6_var = ctk.BooleanVar(value=s.disable_ipv6_while_active)
    webrtc_var = ctk.BooleanVar(value=s.harden_webrtc_enabled)
    lan_var = ctk.BooleanVar(value=s.lan_lockdown_enabled)
    telemetry_var = ctk.BooleanVar(value=s.telemetry_kill_enabled)

    def _priv_toggle(parent: Any, text: str, var: ctk.BooleanVar, tip: str = "") -> ctk.CTkCheckBox:
        cb = ctk.CTkCheckBox(
            parent, text=text, variable=var, font=(FONT, 11),
            fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT,
        )
        cb.pack(anchor="w", padx=12, pady=6)
        return cb

    _priv_toggle(
        toggles_card, "Randomize MAC addresses on physical adapters", mac_var,
        "Changes NIC MAC values while chain runs (admin required).",
    )
    _priv_toggle(
        toggles_card, "Re-randomize MAC on EVERY chain rotation (paranoid)", mac_rotate_var,
        "Mutates MAC every time the chain rotates — prevents long-session correlation.",
    )
    _priv_toggle(
        toggles_card, "Spoof computer / NetBIOS hostname", host_var,
        "Temporarily renames machine identity while active (admin required).",
    )
    _priv_toggle(
        toggles_card, "Flush DNS cache when starting or rotating chains", dns_flush_var,
        "Clears cached resolver results to reduce stale direct routes.",
    )
    _priv_toggle(
        toggles_card, "Disable IPv6 on active adapters", ipv6_var,
        "Turns off IPv6 bindings while active to reduce IPv6 leak paths.",
    )
    _priv_toggle(
        toggles_card,
        "Harden WebRTC in Chrome / Edge (block non-proxied UDP)",
        webrtc_var,
        "Applies Windows policy to block direct WebRTC UDP bypass in Chromium browsers.",
    )
    _priv_toggle(
        toggles_card,
        "LAN lockdown: kill LLMNR / NetBIOS / mDNS hostname broadcasts",
        lan_var,
        "Stops your machine from advertising its hostname on the local network "
        "(admin required, reversible on stop).",
    )
    _priv_toggle(
        toggles_card,
        "Telemetry kill: DiagTrack, Activity History, Cortana web, ad ID",
        telemetry_var,
        "Stops Windows telemetry pipelines while chain is up. Reversible on stop "
        "(snapshot of original state taken).",
    )

    fp_card = _priv_section(
        "Device fingerprint",
        "OS-level identifiers. Browsers still fingerprint Canvas, fonts, and WebGL separately.",
    )
    fp_box = ctk.CTkTextbox(fp_card, height=140, font=("Consolas", 10),
                            fg_color=BG, text_color=TEXT,
                            scrollbar_button_color=ACCENT)
    fp_box.pack(fill="x", padx=12, pady=(4, 8))

    def _refresh_fingerprint() -> None:
        fp_box.delete("1.0", "end")
        audit = audit_device()
        fp_box.insert("end", "\n".join(audit.lines))

    _btn(fp_card, "Refresh fingerprint audit", _refresh_fingerprint, w=180, h=28).pack(
        anchor="w", padx=12, pady=(0, 10))

    # ── DNS leak test ────────────────────────────────────────────────────────
    dns_card = _priv_section(
        "DNS leak test",
        "Compares how DNS resolves through your chain vs direct.  Detects resolver bypass.",
    )
    dns_box = ctk.CTkTextbox(
        dns_card, height=120, font=("Consolas", 10),
        fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT,
    )
    dns_box.pack(fill="x", padx=12, pady=(4, 4))
    dns_box.insert("end", "Click 'Full DNS leak test' or 'Quick check' to probe.")
    dns_status_lbl = ctk.CTkLabel(dns_card, text="", font=(FONT, 10, "bold"), text_color=TEXT2)
    dns_status_lbl.pack(anchor="w", padx=12, pady=(0, 2))

    def _render_dns_report(rep: FullDnsLeakReport) -> None:
        dns_box.delete("1.0", "end")
        mark = "LEAK" if rep.leaked else "OK  "
        dns_box.insert("end", f"[{mark}] {rep.summary}\n\n")
        if rep.system_resolvers:
            dns_box.insert("end", f"Configured resolvers: {', '.join(rep.system_resolvers)}\n")
        for host, addrs in rep.direct_resolution.items():
            via_proxy = rep.proxy_resolution.get(host, [])
            proxy_str = ", ".join(via_proxy) if via_proxy else "(no proxy answer)"
            dns_box.insert("end", f"  {host:<22} direct={', '.join(addrs) or '—':<20} via-proxy={proxy_str}\n")
        if rep.errors:
            dns_box.insert("end", "\nErrors: " + "; ".join(rep.errors[:3]))
        color = RED if rep.leaked else GREEN
        dns_status_lbl.configure(
            text="DNS LEAK DETECTED" if rep.leaked else "DNS looks clean",
            text_color=color,
        )

    def _run_full_dns_test() -> None:
        proxy = f"http://{svc.settings.listen_addr()}" if svc.current_chain else None
        dns_status_lbl.configure(text="Running…", text_color=YELLOW)
        dns_box.delete("1.0", "end")
        dns_box.insert("end", "Probing DNS via proxy vs direct…\n")

        def work() -> None:
            try:
                rep = run_dns_leak_test(proxy_url=proxy,
                                        timeout=min(12.0, svc.settings.validation_timeout_seconds))
            except Exception as e:
                root.after(0, lambda: dns_status_lbl.configure(
                    text=f"Error: {e}", text_color=RED))
                return
            root.after(0, lambda: _render_dns_report(rep))

        threading.Thread(target=work, daemon=True).start()

    def _run_quick_dns_check() -> None:
        listen = f"http://{svc.settings.listen_addr()}" if svc.current_chain else None
        r = check_dns_leak_hint(listen)
        color = GREEN if r.ok else RED
        dns_box.delete("1.0", "end")
        dns_box.insert("end", r.message)
        if r.system_resolvers:
            dns_box.insert("end", f"\nResolvers: {', '.join(r.system_resolvers)}")
        dns_status_lbl.configure(text="clean" if r.ok else "issue detected", text_color=color)

    def _manual_dns_flush() -> None:
        ok, msg = flush_dns_cache()
        dns_status_lbl.configure(text=msg, text_color=GREEN if ok else RED)

    dns_btn_row = ctk.CTkFrame(dns_card, fg_color="transparent")
    dns_btn_row.pack(fill="x", padx=12, pady=(0, 10))
    _btn(dns_btn_row, "Full DNS leak test", _run_full_dns_test, w=140, h=28,
         fg_color=ACCENT2, hover_color=ACCENT).pack(side="left", padx=(0, 6))
    _btn(dns_btn_row, "Quick check", _run_quick_dns_check, w=100, h=28).pack(side="left", padx=(0, 6))
    _btn(dns_btn_row, "Flush DNS now", _manual_dns_flush, w=110, h=28,
         fg_color=DIM).pack(side="left")

    # ── WebRTC leak test ─────────────────────────────────────────────────────
    rtc_card = _priv_section(
        "WebRTC leak test",
        "Scans Chrome/Edge policy, Firefox profile prefs, and live STUN server reachability.",
    )
    rtc_box = ctk.CTkTextbox(
        rtc_card, height=110, font=("Consolas", 10),
        fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT,
    )
    rtc_box.pack(fill="x", padx=12, pady=(4, 4))
    rtc_box.insert("end", "Click 'WebRTC leak test' to probe.")
    rtc_status_lbl = ctk.CTkLabel(rtc_card, text="", font=(FONT, 10, "bold"), text_color=TEXT2)
    rtc_status_lbl.pack(anchor="w", padx=12, pady=(0, 2))

    def _render_rtc_report(rep: WebRtcCheckResult) -> None:
        rtc_box.delete("1.0", "end")
        for name, ok, detail in rep.checks:
            mark = "OK  " if ok else "RISK"
            rtc_box.insert("end", f"[{mark}] {name:<40}  {detail}\n")
        color = RED if rep.any_leak_risk else GREEN
        rtc_status_lbl.configure(
            text="WebRTC LEAK RISK — see items above" if rep.any_leak_risk else "WebRTC looks protected",
            text_color=color,
        )

    def _run_rtc_test() -> None:
        rtc_status_lbl.configure(text="Running…", text_color=YELLOW)
        rtc_box.delete("1.0", "end")
        rtc_box.insert("end", "Scanning WebRTC surfaces…\n")
        profile = Path(firefox_profile_var.get().strip() or default_profile_dir())

        def work() -> None:
            try:
                rep = run_webrtc_check(profile_dir=profile)
            except Exception as e:
                root.after(0, lambda: rtc_status_lbl.configure(text=f"Error: {e}", text_color=RED))
                return
            root.after(0, lambda: _render_rtc_report(rep))

        threading.Thread(target=work, daemon=True).start()

    rtc_btn_row = ctk.CTkFrame(rtc_card, fg_color="transparent")
    rtc_btn_row.pack(fill="x", padx=12, pady=(0, 10))
    _btn(rtc_btn_row, "WebRTC leak test", _run_rtc_test, w=140, h=28,
         fg_color=ACCENT2, hover_color=ACCENT).pack(side="left")

    wipe_card = _priv_section(
        "Forensic artifact wipe",
        "One-button purge of common Windows breadcrumb trails. Irreversible.",
    )
    wipe_result_lbl = ctk.CTkLabel(
        wipe_card, text="Click 'Wipe now' to purge %TEMP%, Recent, Jump Lists, "
        "Prefetch (Admin), MRU lists, and the clipboard.",
        font=(FONT, 10), text_color=TEXT2, wraplength=880, justify="left",
    )
    wipe_result_lbl.pack(anchor="w", padx=12, pady=8)

    def _do_wipe() -> None:
        wipe_result_lbl.configure(text="Wiping…", text_color=YELLOW)

        def work() -> None:
            try:
                rep = wipe_artifacts(include_prefetch=True)
            except Exception as e:
                root.after(0, lambda: wipe_result_lbl.configure(
                    text=f"Wipe failed: {e}", text_color=RED))
                return
            root.after(0, lambda: wipe_result_lbl.configure(
                text=rep.summary(), text_color=GREEN if not rep.errors else YELLOW))

        threading.Thread(target=work, daemon=True).start()

    _btn(wipe_card, "Wipe now", _do_wipe, w=110, h=28,
         fg_color=RED, hover_color="#8a1d1d").pack(anchor="w", padx=12, pady=(0, 10))

    audit_card = _priv_section(
        "Leak audit  (mission-critical)",
        "Probes every leak surface: IP, DNS, IPv6, WPAD, Group Policy, "
        "ProxySettingsPerUser, LLMNR / NetBIOS / mDNS, VPN, WebRTC policy.",
    )
    audit_box = ctk.CTkTextbox(
        audit_card, height=220, font=("Consolas", 10),
        fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT,
    )
    audit_box.pack(fill="x", padx=12, pady=(4, 4))
    audit_box.insert("end", "Click 'Run audit' to probe live leak status.")
    audit_status_lbl = ctk.CTkLabel(
        audit_card, text="", font=(FONT, 11, "bold"), text_color=TEXT2,
    )
    audit_status_lbl.pack(anchor="w", padx=12, pady=(0, 4))

    def _render_audit(rep: AuditReport) -> None:
        audit_box.delete("1.0", "end")
        for f in rep.findings:
            mark = "OK  " if f.ok else "LEAK"
            line = f"[{mark}] {f.name:<34}  {f.value}"
            if f.note:
                line += f"   — {f.note}"
            audit_box.insert("end", line + "\n")
        bad = sum(1 for f in rep.findings if not f.ok)
        if rep.overall_ok:
            audit_status_lbl.configure(
                text="CLEAN — no leaks detected on probed surfaces.", text_color=GREEN
            )
        else:
            audit_status_lbl.configure(
                text=f"{bad} leak(s) detected — see report above.", text_color=RED
            )

    def _run_audit_async() -> None:
        audit_status_lbl.configure(text="Running audit…", text_color=YELLOW)
        audit_box.delete("1.0", "end")
        audit_box.insert("end", "Probing every leak surface in parallel…\n")

        def work() -> None:
            try:
                rep = run_audit_sync(
                    f"http://{svc.settings.listen_addr()}",
                    svc.settings.ip_check_url,
                    timeout_seconds=min(12.0, svc.settings.validation_timeout_seconds),
                )
            except Exception as e:
                root.after(0, lambda: audit_status_lbl.configure(
                    text=f"Audit failed: {e}", text_color=RED))
                return
            root.after(0, lambda: _render_audit(rep))

        threading.Thread(target=work, daemon=True).start()

    audit_btn_row = ctk.CTkFrame(audit_card, fg_color="transparent")
    audit_btn_row.pack(fill="x", padx=12, pady=(0, 10))
    _btn(audit_btn_row, "Run audit", _run_audit_async, w=120, h=28,
         fg_color=ACCENT2, hover_color=ACCENT).pack(side="left", padx=(0, 8))

    _btn(priv_scroll, "Save privacy settings", lambda: _save_settings(verbose=True),
         w=200, h=34, font=(FONT, 12)).pack(anchor="w", padx=8, pady=12)

    # ─── TAB: SETTINGS ───────────────────────────────────────────────────────
    sf_outer = ctk.CTkScrollableFrame(tab_settings, fg_color=BG,
                                      scrollbar_button_color=ACCENT)
    sf_outer.pack(fill="both", expand=True)

    def _section(title: str) -> ctk.CTkFrame:
        ctk.CTkLabel(sf_outer, text=title, font=(FONT, 12, "bold"),
                     text_color=CYAN).pack(anchor="w", padx=6, pady=(14, 4))
        f = ctk.CTkFrame(sf_outer, fg_color=CARD, corner_radius=8)
        f.pack(fill="x", padx=6, pady=(0, 4))
        return f

    entries: dict[str, ctk.CTkEntry] = {}

    def _fld(parent: Any, label: str, key: str, val: str, tip: str = "") -> None:
        row = ctk.CTkFrame(parent, fg_color="transparent")
        row.pack(fill="x", padx=12, pady=5)
        ctk.CTkLabel(row, text=label, font=(FONT, 11), text_color=TEXT,
                     width=230, anchor="w").pack(side="left")
        e = ctk.CTkEntry(row, height=26, font=(FONT, 11),
                         fg_color=BG, border_color=ACCENT, width=130)
        e.insert(0, val)
        e.pack(side="left")
        if tip:
            ctk.CTkLabel(row, text=f"  {tip}", font=(FONT, 10),
                         text_color=TEXT2).pack(side="left")
        entries[key] = e

    net = _section("Network")
    _fld(net, "Local listener port", "port", str(s.local_port), "(point apps here)")
    _fld(net, "Proxy bypass list", "bypass", s.proxy_bypass, "(semicolons)")
    _fld(net, "IP check URL", "check_url", s.ip_check_url)

    timing = _section("Timing")
    _fld(timing, "Health check interval (sec)", "health", str(s.health_check_seconds),
         "(how often exit IP is re-verified)")
    _fld(timing, "Full pool refresh (sec)", "refresh", str(s.full_refresh_seconds),
         "(re-fetch + re-validate)")

    val_sec = _section("Validation")
    _fld(val_sec, "Concurrent validators", "conc", str(s.validation_concurrency))
    _fld(val_sec, "Stop after N valid proxies found", "minpool", str(s.min_pool_size),
         "(early-exit — stop testing once enough found)")
    _fld(val_sec, "Max candidates to test per cycle", "maxc", str(s.max_candidates),
         "(shuffled random sample from fetch)")
    _fld(val_sec, "Per-proxy timeout (sec)", "timeout", str(s.validation_timeout_seconds))

    sec_sec = _section("Security")
    ks_row = ctk.CTkFrame(sec_sec, fg_color="transparent")
    ks_row.pack(fill="x", padx=12, pady=8)
    ks_var = ctk.BooleanVar(value=s.kill_switch_enabled)
    ctk.CTkCheckBox(
        ks_row,
        text="Firewall kill-switch  (block ALL traffic if chain is down — requires Admin)",
        variable=ks_var, font=(FONT, 11),
        fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT,
    ).pack(side="left")

    # Emergency disengage — for crashes where the kill-switch lingers and
    # the operator needs internet back fast without restarting the app.
    def _emergency_disengage_now() -> None:
        from tkinter import messagebox
        from .firewall import emergency_disengage as _ed, is_engaged as _ie
        if not _ie():
            messagebox.showinfo("Kill-switch", "No Proxy God firewall rules detected — nothing to disengage.")
            return
        if not messagebox.askyesno(
            "Emergency disengage",
            "Remove ALL Proxy God firewall rules and restore normal outbound traffic?\n\n"
            "Use this only when the chain has dropped and the kill-switch is "
            "blocking everything.",
        ):
            return
        _ed()
        messagebox.showinfo("Kill-switch", "Firewall rules removed.")
        _log("Emergency disengage: firewall rules removed by operator.")

    ks_btn_row = ctk.CTkFrame(sec_sec, fg_color="transparent")
    ks_btn_row.pack(fill="x", padx=12, pady=(0, 8))
    _btn(
        ks_btn_row, "⚠ Emergency disengage firewall now",
        _emergency_disengage_now,
        w=320, h=28,
        fg_color="#7f1d1d", hover_color="#991b1b",
    ).pack(side="left")

    def _apply_point_and_shoot() -> None:
        """Simple safe defaults: secure + low-friction launch profile."""
        use_manual_var.set(True)
        mode_var.set("auto")
        elite_var.set(False)
        ks_var.set(True)
        dns_flush_var.set(True)
        mac_var.set(False)
        mac_rotate_var.set(False)
        host_var.set(False)
        ipv6_var.set(False)
        webrtc_var.set(True)
        lan_var.set(False)
        telemetry_var.set(False)
        persona_var.set(PERSONA_LABELS["blend_windows_chrome"])
        cookie_var.set(COOKIE_LABELS["block_third_party"])
        br_force_proxy_var.set(True)
        br_disable_webrtc_var.set(True)
        br_rfp_var.set(True)
        br_telemetry_var.set(True)
        br_fpi_var.set(True)
        br_strict_tp_var.set(True)
        br_tz_utc_var.set(True)
        br_clear_var.set(True)
        br_disposable_var.set(False)
        br_kill_drop_var.set(True)
        br_auto_relaunch_var.set(False)
        br_lock_profile_var.set(True)
        if not firefox_path_var.get().strip():
            firefox_path_var.set(default_firefox_path())
        if not firefox_profile_var.get().strip():
            firefox_profile_var.set(default_profile_dir())
        _save_settings(verbose=False)
        _log("⚡ Point-and-shoot preset applied.")

    ready_btn = _btn(
        topbar,
        "⚡ Ready",
        _apply_point_and_shoot,
        w=78,
        fg_color="#0f766e",
        hover_color="#0d9488",
    )
    ready_btn.pack(side="left", padx=(8, 2))

    ctk.CTkFrame(sf_outer, fg_color="transparent", height=4).pack()
    _btn(sf_outer, "Save All Settings",
         lambda: _save_settings(verbose=True), w=220, h=36,
         font=(FONT, 13)).pack(anchor="w", padx=6, pady=8)

    # ─────────────────────────────────────────────────────────────────────────
    # SAVE SETTINGS
    # ─────────────────────────────────────────────────────────────────────────
    def _save_settings(verbose: bool = False) -> None:
        try:
            src_raw = sources_box.get("1.0", "end").strip()
            src_list = [ln.strip() for ln in src_raw.splitlines()
                        if ln.strip().startswith("http")]

            n_hops = max(1, len(manual_chain)) if manual_chain else 1

            ns = Settings(
                local_host=LISTEN_HOST,
                local_port=int(entries["port"].get().strip()),
                chain_length=n_hops,
                obfuscation_mode=mode_var.get(),
                use_pinned_chain=bool(use_manual_var.get()),
                pinned_chain=list(manual_chain),
                manual_exit_proxy=_exit_url(),
                health_check_seconds=min(3600, max(10, int(entries["health"].get().strip()))),
                full_refresh_seconds=min(86400, max(60, int(entries["refresh"].get().strip()))),
                validation_concurrency=max(1, int(entries["conc"].get().strip())),
                min_pool_size=max(3, int(entries["minpool"].get().strip())),
                max_candidates=max(10, int(entries["maxc"].get().strip())),
                validation_timeout_seconds=min(120.0, max(2.0, float(entries["timeout"].get().strip()))),
                prefer_elite=bool(elite_var.get()),
                kill_switch_enabled=bool(ks_var.get()),
                proxy_bypass=entries["bypass"].get().strip() or Settings().proxy_bypass,
                sources=src_list or Settings().sources,
                ip_check_url=entries["check_url"].get().strip() or Settings().ip_check_url,
                mac_spoof_enabled=bool(mac_var.get()),
                mac_rotate_on_chain_rotate=bool(mac_rotate_var.get()),
                spoof_hostname_enabled=bool(host_var.get()),
                flush_dns_on_rotate=bool(dns_flush_var.get()),
                disable_ipv6_while_active=bool(ipv6_var.get()),
                harden_webrtc_enabled=bool(webrtc_var.get()),
                lan_lockdown_enabled=bool(lan_var.get()),
                telemetry_kill_enabled=bool(telemetry_var.get()),
                firefox_path=firefox_path_var.get().strip(),
                firefox_profile_dir=firefox_profile_var.get().strip(),
                browser_clear_on_close=bool(br_clear_var.get()),
                browser_disposable_profile=bool(br_disposable_var.get()),
                browser_kill_on_chain_drop=bool(br_kill_drop_var.get()),
                browser_auto_relaunch=bool(br_auto_relaunch_var.get()),
                browser_lock_managed_profile=bool(br_lock_profile_var.get()),
                browser_force_proxy=bool(br_force_proxy_var.get()),
                browser_disable_webrtc=bool(br_disable_webrtc_var.get()),
                browser_resist_fingerprinting=bool(br_rfp_var.get()),
                browser_disable_telemetry=bool(br_telemetry_var.get()),
                browser_first_party_isolation=bool(br_fpi_var.get()),
                browser_strict_tracking_protection=bool(br_strict_tp_var.get()),
                browser_timezone_utc=bool(br_tz_utc_var.get()),
                browser_persona=_persona_key(),
                browser_cookie_mode=_cookie_key(),
            )
            if not (1 <= ns.local_port <= 65535):
                raise ValueError("Port must be 1-65535")
            svc.update_settings(ns)
            save_settings(ns)
            proxy_lbl.configure(text=f"{LISTEN_HOST}:{ns.local_port}")
            if verbose:
                _log("✓ Settings saved.")
        except Exception as e:
            _log(f"⚠ Settings error: {e!s}")

    # ─────────────────────────────────────────────────────────────────────────
    # STATUS REFRESHERS
    # ─────────────────────────────────────────────────────────────────────────
    def _refresh_sysproxy() -> None:
        on = is_system_proxy_set()
        sys_lbl.configure(text="SYS:ON" if on else "SYS:OFF",
                          text_color=GREEN if on else DIM)

    def _refresh_fw(engaged: bool | None = None) -> None:
        if engaged is None:
            engaged = fw_is_engaged()
        fw_lbl.configure(text="FW:ON" if engaged else "FW:OFF",
                         text_color=GREEN if engaged else DIM)

    # ─────────────────────────────────────────────────────────────────────────
    # MESSAGE PUMP
    # ─────────────────────────────────────────────────────────────────────────
    def _handle(m: dict[str, Any]) -> None:
        t = m.get("type")

        if t == "log":
            _log(str(m.get("text", "")))

        elif t == "state":
            running = bool(m.get("running"))
            service_running[0] = running
            status_dot.configure(text_color=GREEN if running else RED)
            status_txt.configure(text="RUNNING" if running else "STOPPED")
            _refresh_sysproxy()
            if running:
                tray.set_state("yellow", exit_ip=None)
            if not running:
                prog_bar.set(0)
                phase_lbl.configure(text="idle")
                countdown_lbl.configure(text="next: —", text_color=TEXT2)
                tray.set_state("gray", exit_ip=None)
                v_exit_ip.set("—")
                v_rotation.set("0")

        elif t == "phase":
            ph = str(m.get("phase", ""))
            phase_labels = {
                "validate": "validate",
                "fetch": "fetch",
                "running": "running",
                "exit_check": "fixed exit",
                "verify_chain": "verify chain",
                "gost_start": "starting chain",
            }
            phase_lbl.configure(text=phase_labels.get(ph, ph or "idle"))
            if ph == "validate":
                prog_bar.configure(progress_color=YELLOW)
            elif ph == "fetch":
                prog_bar.configure(progress_color=ORANGE)
                prog_bar.set(0.15)
            elif ph == "exit_check":
                prog_bar.configure(progress_color=YELLOW)
                prog_bar.set(0.96)
            elif ph == "gost_start":
                prog_bar.configure(progress_color=GREEN)
                prog_bar.set(0.985)
            elif ph == "verify_chain":
                prog_bar.configure(progress_color=GREEN)
                prog_bar.set(0.98)
            elif ph == "running":
                prog_bar.configure(progress_color=GREEN)
                prog_bar.set(1.0)
            elif ph in ("idle", ""):
                prog_bar.set(0)

        elif t == "validate_progress":
            d, tot = int(m.get("done", 0)), max(1, int(m.get("total", 1)))
            prog_bar.set(d / tot)

        elif t == "pool":
            cnt = int(m.get("count", 0))
            pool_lbl.configure(text=f"pool: {cnt}")
            v_pool.set(str(cnt))

        elif t == "real_ip":
            ip = str(m.get("ip", "—"))
            your_ip_lbl.configure(text=f"Direct IP: {ip}")
            v_real_ip.set(ip)
            last_chain_state["direct_ip"] = ip
            root.after(300, lambda: _refresh_chain_map(silent=True))

        elif t == "vpn":
            nonlocal outer_hop_label
            if m.get("active"):
                outer_hop_label = str(m.get("label", "VPN"))[:12]
                vpn_status_lbl.configure(
                    text=f"VPN: {m.get('label', 'Active')}"
                    + (f"  ({m.get('adapter')})" if m.get("adapter") else ""),
                    text_color=GREEN,
                )
                leak_mode_lbl.configure(
                    text=f"Leak check: {m.get('leak_mode', 'VPN-aware')}",
                    text_color=TEXT2,
                )
            else:
                outer_hop_label = "Direct"
                vpn_status_lbl.configure(
                    text="VPN: not detected (direct connection)", text_color=YELLOW,
                )
                leak_mode_lbl.configure(
                    text=f"Leak check: {m.get('leak_mode', 'strict')}",
                    text_color=TEXT2,
                )

        elif t == "firewall":
            _refresh_fw(bool(m.get("engaged")))

        elif t == "rotation":
            n = str(m.get("n", 0))
            rotation_lbl.configure(text=f"rot: {n}")
            v_rotation.set(n)

        elif t == "countdown":
            secs = int(m.get("secs", 0))
            if secs > 0:
                m_s, s_s = divmod(secs, 60)
                countdown_lbl.configure(
                    text=f"next: {m_s}m{s_s:02d}s",
                    text_color=YELLOW if secs < 30 else TEXT2,
                )
            else:
                countdown_lbl.configure(text="next: checking…", text_color=YELLOW)

        elif t == "hops":
            hops = [str(x) for x in (m.get("hops") or [])]
            status = str(m.get("status", "connecting"))
            exit_ip = m.get("exit_ip")
            exit_s = str(exit_ip) if exit_ip else None
            # If the live check hasn't resolved yet, show the explicit final-hop IP
            # (when available) so tunnel/fixed-exit setups still expose the egress node.
            if not exit_s and hops:
                exit_s = _extract_ip_host(hops[-1])
            fix = normalize_proxy_url(svc.settings.manual_exit_proxy)
            fixed_last = bool(fix) and not svc.settings.use_pinned_chain
            _render_chain(hops, status, exit_s, fixed_last=fixed_last)
            _refresh_sysproxy()
            tray_color = {"healthy": "green", "dead": "red", "connecting": "yellow"}.get(
                status, "yellow"
            )
            tray.set_state(tray_color, exit_ip=exit_s)
            prev_exit = v_exit_ip.get()
            v_exit_ip.set(exit_s if exit_s else "—")
            last_chain_state["hops"] = hops
            last_chain_state["status"] = status
            last_chain_state["exit_ip"] = exit_s or ""
            root.after(250, lambda: _refresh_chain_map(silent=True))
            if status == "healthy" and exit_s and exit_s != prev_exit:
                root.after(400, lambda: _refresh_exit_intel(silent=True))
            elif status != "healthy":
                _apply_intel(None)
            if status == "dead" and browser.is_running() and bool(br_kill_drop_var.get()):
                browser.stop(dispose=bool(br_disposable_var.get()))
                _refresh_browser_status()
                _log("Browser stopped because chain dropped (Kill-on-drop enabled).")

    def _pump() -> None:
        try:
            while True:
                _handle(ui_q.get_nowait())
        except queue.Empty:
            pass
        if browser_should_run[0] and not browser.is_running():
            cfg = _browser_cfg()
            now = time.monotonic()
            max_retries = max(0, int(svc.settings.max_browser_relaunches))
            if cfg.auto_relaunch and (now - browser_last_launch_ts[0]) >= 3.0:
                if browser_relaunch_count[0] >= max_retries:
                    browser_should_run[0] = False
                    _log(
                        f"Browser auto-relaunch disabled after "
                        f"{browser_relaunch_count[0]} attempts — fix the profile "
                        "or restart manually."
                    )
                elif _launch_browser_with_cfg(cfg):
                    browser_last_launch_ts[0] = now
                    browser_relaunch_count[0] += 1
        root.after(80, _pump)

    # ─────────────────────────────────────────────────────────────────────────
    # CLOSE / MINIMIZE
    # ─────────────────────────────────────────────────────────────────────────
    def _close() -> None:
        _cancel_pulse()
        browser_should_run[0] = False
        browser.stop(dispose=bool(br_disposable_var.get()))
        tray.stop()
        svc.stop()
        clear_system_proxy()
        if is_admin() and fw_is_engaged():
            fw_disengage()
        root.destroy()

    def _on_window_x() -> None:
        """X-button handler: warn the operator that the chain is still active
        before silently going to tray, then offer to fully quit instead."""
        chain_alive = bool(svc.current_chain) or fw_is_engaged()
        if chain_alive:
            from tkinter import messagebox
            choice = messagebox.askyesnocancel(
                "Proxy God still running",
                "The proxy chain and/or kill-switch are still active.\n\n"
                "  • Yes  — minimize to tray (chain keeps running)\n"
                "  • No   — fully quit (stop chain + remove firewall rules)\n"
                "  • Cancel — keep window open",
            )
            if choice is None:
                return
            if choice is False:
                _close()
                return
        root.withdraw()

    root.protocol("WM_DELETE_WINDOW", _on_window_x)

    # ─────────────────────────────────────────────────────────────────────────
    # INIT
    # ─────────────────────────────────────────────────────────────────────────
    _refresh_boot()
    _refresh_sysproxy()
    _refresh_fw()
    _refresh_vpn_ui()
    _refresh_browser_status()
    _refresh_fingerprint()
    _log("─" * 60)
    _log("Proxy God v2 — ready.")
    _log(f"Admin: {'YES — kill-switch + privacy hardening available' if is_admin() else 'NO — run as Admin for full privacy tools'}")
    _log(f"Log file: {LOG_PATH}")
    _log("Press START to fetch, validate and chain proxies.")
    _log("Chain Builder → build your chain, Test entire chain, then Start.")
    _log("Browser tab → launch hardened Firefox profile that follows your chain.")
    _log("Signup Prep → open signup pages with autofill (Google / Proton / HydraProxy / custom).")
    _log("Privacy tab → MAC, hostname, IPv6, WebRTC, fingerprint audit, DNS checks.")
    _log("Works with or without VPN — leak detection adapts automatically.")
    _log("─" * 60)
    _pump()
    root.mainloop()


if __name__ == "__main__":
    main()
