"""
Proxy God — GUI
Tabs: Live | Chain Builder | Settings
"""
from __future__ import annotations

import logging
import queue
import sys
from datetime import datetime
from typing import Any

import customtkinter as ctk

from .config import (
    LISTEN_HOST,
    OBFUSCATION_LABELS,
    OBFUSCATION_MODES,
    Settings,
    load_settings,
    normalize_proxy_url,
    save_settings,
)
from .firewall import (
    disengage as fw_disengage,
    is_admin,
    is_engaged as fw_is_engaged,
    request_admin_relaunch,
)
from .paths import app_data_dir
from .service import ChainService
from .sysproxy import clear_system_proxy, is_system_proxy_set
from .tray import TrayIcon
from .windows_task import install_logon_task, task_exists, uninstall_logon_task

LOG_PATH = app_data_dir() / "proxy_chain_manager.log"
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    handlers=[
        logging.FileHandler(LOG_PATH, encoding="utf-8"),
        logging.StreamHandler(sys.stdout),
    ],
)

# ── palette ──────────────────────────────────────────────────────────────────
BG      = "#0d0d1a"
CARD    = "#12122a"
PANEL   = "#1a1a3e"
ACCENT  = "#1e3a8a"
ACCENT2 = "#2563eb"
GREEN   = "#00e676"
RED     = "#ff1744"
YELLOW  = "#ffab00"
ORANGE  = "#ff6d00"
PURPLE  = "#bb86fc"
BLUE    = "#448aff"
CYAN    = "#00bcd4"
DIM     = "#4a5568"
TEXT    = "#e2e8f0"
TEXT2   = "#94a3b8"
FONT    = "Segoe UI"


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


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

    root = ctk.CTk()
    root.title("Proxy God  v2")
    root.geometry("1080x740")
    root.minsize(900, 620)
    root.configure(fg_color=BG)

    ui_q: queue.Queue[dict[str, Any]] = queue.Queue()
    svc = ChainService(notify=lambda m: ui_q.put(m))
    s = load_settings()

    # ── 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()

    # ─────────────────────────────────────────────────────────────────────────
    # SHARED STATE
    # ─────────────────────────────────────────────────────────────────────────
    _hop_count = [s.chain_length]   # mutable ref so topbar buttons can mutate it

    pulse_job: dict[str, str | None] = {"id": None}

    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 = u.replace("http://", "").replace("socks5://", "s5://").replace("socks4://", "s4://").replace("https://", "")
        return u[:28] + "…" if len(u) > 30 else u

    # ─────────────────────────────────────────────────────────────────────────
    # HELPERS
    # ─────────────────────────────────────────────────────────────────────────
    def _btn(parent: Any, label: str, cmd: Any, w: int = 80, h: int = 28, **kw: Any) -> ctk.CTkButton:
        cfg: dict[str, Any] = {"fg_color": ACCENT, "hover_color": ACCENT2, "text_color": TEXT}
        cfg.update(kw)
        return ctk.CTkButton(
            parent, text=label, command=cmd, width=w, height=h,
            font=(FONT, 11), corner_radius=6, **cfg,
        )

    # ─────────────────────────────────────────────────────────────────────────
    # 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()
        svc.start()

    _btn(topbar, "▶ Start",  _start,        w=86).pack(side="left", padx=2)
    _btn(topbar, "■ Stop",   svc.stop,       w=76).pack(side="left", padx=2)
    _btn(topbar, "↻ Rotate", svc.rotate_now, w=76).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))

    # Hop count +/- control
    ctk.CTkLabel(topbar, text="│", text_color=DIM).pack(side="left", padx=6)
    ctk.CTkLabel(topbar, text="Hops:", font=(FONT, 11), text_color=TEXT2).pack(side="left", padx=(0, 2))

    hop_count_lbl = ctk.CTkLabel(topbar, text=str(_hop_count[0]),
                                  font=(FONT, 15, "bold"), text_color=CYAN, width=24)
    hop_count_lbl.pack(side="left")

    def _hop_adjust(delta: int) -> None:
        new = max(1, min(8, _hop_count[0] + delta))
        _hop_count[0] = new
        hop_count_lbl.configure(text=str(new))
        # Update hop slider in Chain Builder if it exists
        try:
            hop_slider.set(new)
            hop_val_lbl.configure(text=str(new))
        except Exception:
            pass
        # Immediately apply to settings
        _apply_hop_count(new)

    _btn(topbar, "−", lambda: _hop_adjust(-1), w=26, h=26,
         fg_color=PANEL, hover_color=ACCENT).pack(side="left", padx=1)
    _btn(topbar, "+", lambda: _hop_adjust(+1), w=26, h=26,
         fg_color=PANEL, hover_color=ACCENT).pack(side="left", padx=(1, 6))

    def _apply_hop_count(n: int) -> None:
        """Save hop count change to live settings immediately."""
        cur = svc.settings
        ns = Settings(
            local_host=cur.local_host,
            local_port=cur.local_port,
            chain_length=n,
            obfuscation_mode=cur.obfuscation_mode,
            use_pinned_chain=cur.use_pinned_chain,
            pinned_chain=cur.pinned_chain,
            manual_exit_proxy=cur.manual_exit_proxy,
            health_check_seconds=cur.health_check_seconds,
            full_refresh_seconds=cur.full_refresh_seconds,
            validation_concurrency=cur.validation_concurrency,
            max_candidates=cur.max_candidates,
            validation_timeout_seconds=cur.validation_timeout_seconds,
            prefer_elite=cur.prefer_elite,
            kill_switch_enabled=cur.kill_switch_enabled,
            proxy_bypass=cur.proxy_bypass,
            sources=cur.sources,
            ip_check_url=cur.ip_check_url,
        )
        svc.update_settings(ns)

    # 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:
            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] = []

    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("NORD", 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_settings = tabs.add("  Settings  ")
    for tab in (tab_live, tab_chain, 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, "Your IP  (through Nord)", 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)

    # 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", 11),
        fg_color=BG, text_color=TEXT,
        scrollbar_button_color=ACCENT,
    )
    log_box.pack(fill="both", expand=True, padx=4, pady=4)

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

    # ─── 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)

    ctk.CTkFrame(cb_left, fg_color="transparent", height=8).pack()
    ctk.CTkLabel(cb_left, text="Hop Count  (also: topbar +/−)",
                 font=(FONT, 12, "bold"), text_color=TEXT).pack(anchor="w", padx=12)

    hop_val_lbl = ctk.CTkLabel(cb_left, text=str(_hop_count[0]),
                               font=(FONT, 26, "bold"), text_color=CYAN)
    hop_val_lbl.pack(pady=(2, 0))

    def _on_hop_slider(val: float) -> None:
        n = int(val)
        _hop_count[0] = n
        hop_val_lbl.configure(text=str(n))
        hop_count_lbl.configure(text=str(n))
        _apply_hop_count(n)

    hop_slider = ctk.CTkSlider(
        cb_left, from_=1, to=8, number_of_steps=7,
        command=_on_hop_slider,
        fg_color=PANEL, progress_color=ACCENT2,
        button_color=CYAN, button_hover_color=BLUE,
    )
    hop_slider.set(s.chain_length)
    hop_slider.pack(fill="x", padx=16, pady=(2, 6))

    hop_legend = ctk.CTkFrame(cb_left, fg_color="transparent")
    hop_legend.pack(fill="x", padx=16, pady=(0, 8))
    ctk.CTkLabel(hop_legend, text="1", font=(FONT, 9), text_color=DIM).pack(side="left")
    ctk.CTkLabel(hop_legend, text="8", font=(FONT, 9), text_color=DIM).pack(side="right")

    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))
    ctk.CTkLabel(
        exit_fix_frame,
        text="Fixed exit — last hop only",
        font=(FONT, 11, "bold"),
        text_color=ORANGE,
    ).pack(anchor="w", padx=10, pady=(8, 2))
    exit_proxy_entry = ctk.CTkEntry(
        exit_fix_frame,
        placeholder_text="http://host:port  or  socks5://host:port  (optional)",
        font=("Consolas", 10),
        fg_color=BG,
        border_color=ACCENT,
        height=28,
    )
    exit_proxy_entry.pack(fill="x", padx=10, pady=(0, 4))
    if s.manual_exit_proxy:
        exit_proxy_entry.insert(0, s.manual_exit_proxy)
    ctk.CTkLabel(
        exit_fix_frame,
        text="Earlier hops still rotate randomly. Empty = fully automatic exit.\n"
             "Ignored while “Pin & use this chain” is enabled.",
        font=(FONT, 9),
        text_color=TEXT2,
        justify="left",
    ).pack(anchor="w", padx=10, pady=(0, 8))

    # Right — manual chain editor
    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, 4))
    ctk.CTkLabel(hdr_row, text="Manual Chain",
                 font=(FONT, 12, "bold"), text_color=TEXT).pack(side="left")

    use_manual_var = ctk.BooleanVar(value=s.use_pinned_chain)
    ctk.CTkCheckBox(
        hdr_row, text="Pin & use this chain",
        variable=use_manual_var, font=(FONT, 10),
        fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT2,
    ).pack(side="right")

    manual_chain: list[str] = list(s.pinned_chain)

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

    def _rebuild_chain_ui() -> None:
        for w in list(chain_list_frame.winfo_children()):
            w.destroy()
        for i, hop in enumerate(manual_chain):
            _chain_row(i, hop)

    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)

        ctk.CTkLabel(fr, text=f" {idx+1}.", font=(FONT, 10, "bold"),
                     text_color=CYAN, width=26).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:
            del manual_chain[i]
            _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()

    # 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://ip:port  or  socks5://ip:port",
        font=(FONT, 10), fg_color=BG, border_color=ACCENT, height=28,
    )
    add_entry.pack(side="left", fill="x", expand=True, padx=(0, 4))

    def _add_proxy() -> None:
        v = add_entry.get().strip()
        if not v:
            return
        if "://" not in v:
            v = "http://" + v
        manual_chain.append(v)
        _rebuild_chain_ui()
        add_entry.delete(0, "end")

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

    def _paste_current() -> None:
        for h in svc.current_chain:
            if h not in manual_chain:
                manual_chain.append(h)
        _rebuild_chain_ui()
        _log("Active chain pasted into manual editor.")

    _btn(cb_right, "↙ Paste active chain", _paste_current, w=180, h=26,
         fg_color=DIM, hover_color=ACCENT).pack(pady=(0, 10))

    # 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: 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, "Max candidates per cycle", "maxc", str(s.max_candidates),
         "(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")

    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 = _hop_count[0]
            hop_slider.set(n_hops)
            hop_val_lbl.configure(text=str(n_hops))
            hop_count_lbl.configure(text=str(n_hops))

            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=normalize_proxy_url(exit_proxy_entry.get()),
                health_check_seconds=max(10, int(entries["health"].get().strip())),
                full_refresh_seconds=max(60, int(entries["refresh"].get().strip())),
                validation_concurrency=max(1, int(entries["conc"].get().strip())),
                max_candidates=max(10, int(entries["maxc"].get().strip())),
                validation_timeout_seconds=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,
            )
            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"))
            status_dot.configure(text_color=GREEN if running else RED)
            status_txt.configure(text="RUNNING" if running else "STOPPED")
            _refresh_sysproxy()
            if not running:
                prog_bar.set(0)
                phase_lbl.configure(text="idle")
                countdown_lbl.configure(text="next: —", text_color=TEXT2)
                tray.set_state("gray")
                v_exit_ip.set("—")
                v_rotation.set("0")

        elif t == "phase":
            ph = str(m.get("phase", ""))
            phase_lbl.configure(text=ph)
            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 == "running":
                prog_bar.configure(progress_color=GREEN)
                prog_bar.set(1.0)
            else:
                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"Your IP: {ip}")
            v_real_ip.set(ip)

        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")
            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_ip, fixed_last=fixed_last)
            _refresh_sysproxy()
            tray.set_state(
                {"healthy": "green", "dead": "red", "connecting": "yellow"}.get(status, "yellow")
            )
            v_exit_ip.set(str(exit_ip) if exit_ip else "—")

    def _pump() -> None:
        try:
            while True:
                _handle(ui_q.get_nowait())
        except queue.Empty:
            pass
        root.after(80, _pump)

    # ─────────────────────────────────────────────────────────────────────────
    # CLOSE / MINIMIZE
    # ─────────────────────────────────────────────────────────────────────────
    def _close() -> None:
        _cancel_pulse()
        tray.stop()
        svc.stop()
        clear_system_proxy()
        if is_admin() and fw_is_engaged():
            fw_disengage()
        root.destroy()

    root.protocol("WM_DELETE_WINDOW", lambda: root.withdraw())

    # ─────────────────────────────────────────────────────────────────────────
    # INIT
    # ─────────────────────────────────────────────────────────────────────────
    _refresh_boot()
    _refresh_sysproxy()
    _refresh_fw()
    _log("─" * 60)
    _log("Proxy God v2 — ready.")
    _log(f"Admin: {'YES — kill-switch available' if is_admin() else 'NO — run as Admin for kill-switch'}")
    _log(f"Log file: {LOG_PATH}")
    _log("Press START to fetch, validate and chain proxies.")
    _log("Use +/− in top bar to change hop count instantly.")
    _log("Chain Builder → Fixed exit: set the last hop (optional); rest still rotates.")
    _log("─" * 60)
    _pump()
    root.mainloop()


if __name__ == "__main__":
    main()
