from __future__ import annotations

import ctypes
import os
import re
import threading
import time
from typing import Callable, Dict, Sequence, Tuple

import tkinter as tk
import tkinter.font as tkfont
from tkinter import ttk

from common.runtime_paths import resource_path
from common.shared import (
    ACCENT_BG,
    APP_ICON_FILENAME,
    BORDER_COLOR,
    CONTENT_BG,
    TEXT_BG,
    TAB_PIXEL_HEIGHT,
    TAB_PIXEL_WIDTH,
    TEXT_FG,
)

LOG_PANEL_WIDTH = 320
LOG_TEXT_WIDTH = 38
LOG_TEXT_HEIGHT = 34
LOG_ACTION_BUTTON_WIDTH = 12
LOG_PANEL_PADDING = 6
RESULT_COPY_BUTTON_WIDTH = 14
PAGE_CONTENT_GAP = 10
SECTION_GAP = 8
PANEL_PADDING = 8
RESULT_TEXT_HEIGHT = 10
ACTION_BUTTON_WIDTH = 12
LONG_ACTION_BUTTON_WIDTH = 20
PURCHASE_BUILD_BUTTON_WIDTH = 25
RESOURCE_ACTION_BUTTON_WIDTH = 17
VOLC_PURCHASE_RESOURCE_TREE_ROWS = 6
UI_FONT_FAMILY = "SimSun"
UI_LARGE_FONT_FAMILY = "Microsoft YaHei UI"
APP_DIALOG_FONT = (UI_FONT_FAMILY, 10)
APP_DIALOG_BUTTON_FONT = (UI_FONT_FAMILY, 10)
MANAGER_AUTH_ENTRY_WIDTH_PX = 128
TASK_COMPLETION_SOUND_PATTERN = ((660, 110), (880, 110), (1175, 180))
READONLY_TEXT_ALLOWED_KEYSYMS = {
    "Left",
    "Right",
    "Up",
    "Down",
    "Home",
    "End",
    "Prior",
    "Next",
    "Shift_L",
    "Shift_R",
    "Control_L",
    "Control_R",
    "Alt_L",
    "Alt_R",
    "Escape",
}


def play_task_completion_sound() -> None:
    """Play the selected short completion chime without blocking the Tk UI."""

    def worker() -> None:
        try:
            import winsound

            for index, (frequency, duration_ms) in enumerate(TASK_COMPLETION_SOUND_PATTERN):
                if index:
                    time.sleep(0.035)
                winsound.Beep(int(frequency), int(duration_ms))
        except Exception:
            try:
                import winsound

                winsound.MessageBeep(winsound.MB_ICONASTERISK)
            except Exception:
                pass

    threading.Thread(target=worker, name="task-completion-sound", daemon=True).start()


def build_manager_auth_row(parent: tk.Misc, auth_code_var: tk.StringVar) -> ttk.Entry:
    """Build the centered manager authorization row used by startup notices."""
    auth_frame = ttk.Frame(parent)
    auth_frame.pack(fill=tk.X, pady=(12, 0))
    auth_frame.columnconfigure(0, weight=1, uniform="manager_auth_side")
    auth_frame.columnconfigure(2, weight=1, uniform="manager_auth_side")
    ttk.Label(
        auth_frame,
        text="客户经理授权码:",
        font=(UI_FONT_FAMILY, 10),
    ).grid(row=0, column=0, sticky="e", padx=(0, 4))

    entry_holder = ttk.Frame(auth_frame, width=MANAGER_AUTH_ENTRY_WIDTH_PX)
    entry_holder.grid(row=0, column=1)
    auth_entry = ttk.Entry(entry_holder, textvariable=auth_code_var)
    auth_entry.update_idletasks()
    entry_height = auth_entry.winfo_reqheight()
    entry_holder.configure(width=MANAGER_AUTH_ENTRY_WIDTH_PX, height=entry_height)
    entry_holder.grid_propagate(False)
    auth_entry.place(x=0, y=0, width=MANAGER_AUTH_ENTRY_WIDTH_PX, height=entry_height)

    ttk.Label(
        auth_frame,
        text="（若没有请在软件分享方获取）",
        font=(UI_FONT_FAMILY, 10),
    ).grid(row=0, column=2, sticky="w", padx=(4, 0))
    return auth_entry


def _build_auto_scroll_content(parent: tk.Misc) -> Tuple[ttk.Frame, ttk.Frame]:
    """Build a scrollable content area whose bars appear only when content overflows."""
    shell = ttk.Frame(parent)
    shell.columnconfigure(0, weight=1)
    shell.rowconfigure(0, weight=1)

    canvas = tk.Canvas(shell, bd=0, highlightthickness=0, background=CONTENT_BG)
    canvas.grid(row=0, column=0, sticky="nsew")

    v_scroll = ttk.Scrollbar(shell, orient=tk.VERTICAL, command=canvas.yview, style="App.Vertical.TScrollbar")
    h_scroll = ttk.Scrollbar(shell, orient=tk.HORIZONTAL, command=canvas.xview, style="App.Horizontal.TScrollbar")
    canvas.configure(xscrollcommand=h_scroll.set, yscrollcommand=v_scroll.set)

    content = ttk.Frame(canvas)
    content.columnconfigure(0, weight=1)
    window_id = canvas.create_window((0, 0), window=content, anchor="nw")
    state = {"h_visible": False, "v_visible": False}

    def refresh_scroll_region(_event: tk.Event | None = None) -> None:
        try:
            canvas.update_idletasks()
            content.update_idletasks()
            content_width = max(content.winfo_reqwidth(), 1)
            content_height = max(content.winfo_reqheight(), 1)
            canvas_width = max(canvas.winfo_width(), 1)
            canvas_height = max(canvas.winfo_height(), 1)
            need_h = content_width > canvas_width + 1
            need_v = content_height > canvas_height + 1

            if need_h and not state["h_visible"]:
                h_scroll.grid(row=1, column=0, sticky="ew")
                state["h_visible"] = True
            elif not need_h and state["h_visible"]:
                h_scroll.grid_remove()
                state["h_visible"] = False

            if need_v and not state["v_visible"]:
                v_scroll.grid(row=0, column=1, sticky="ns")
                state["v_visible"] = True
            elif not need_v and state["v_visible"]:
                v_scroll.grid_remove()
                state["v_visible"] = False

            canvas.itemconfigure(window_id, width=max(content_width, canvas_width))
            canvas.itemconfigure(window_id, height=max(content_height, canvas_height))
            canvas.configure(scrollregion=canvas.bbox("all") or (0, 0, canvas_width, canvas_height))
        except Exception:
            pass

    def on_mousewheel(event: tk.Event) -> None:
        if not state["v_visible"]:
            return
        try:
            delta = int(getattr(event, "delta", 0))
            canvas.yview_scroll(-1 * int(delta / 120), "units")
        except Exception:
            pass

    def on_shift_mousewheel(event: tk.Event) -> None:
        if not state["h_visible"]:
            return
        try:
            delta = int(getattr(event, "delta", 0))
            canvas.xview_scroll(-1 * int(delta / 120), "units")
        except Exception:
            pass

    content.bind("<Configure>", refresh_scroll_region)
    canvas.bind("<Configure>", refresh_scroll_region)
    canvas.bind("<MouseWheel>", on_mousewheel)
    canvas.bind("<Shift-MouseWheel>", on_shift_mousewheel)
    return shell, content


def build_cloud_page_layout(parent: tk.Misc) -> Tuple[ttk.Frame, ttk.Frame]:
    """Build the shared cloud page layout: scrollable content left, fixed live log right."""
    parent.columnconfigure(0, weight=1)  # type: ignore[attr-defined]
    parent.columnconfigure(1, weight=0, minsize=LOG_PANEL_WIDTH)  # type: ignore[attr-defined]
    parent.rowconfigure(0, weight=1)  # type: ignore[attr-defined]

    content_shell, content = _build_auto_scroll_content(parent)
    content_shell.grid(row=0, column=0, sticky="nsew", padx=(0, PAGE_CONTENT_GAP))

    log_area = ttk.Frame(parent, width=LOG_PANEL_WIDTH)
    log_area.grid(row=0, column=1, sticky="nsew")
    log_area.grid_propagate(False)
    return content, log_area


def autosize_treeview_columns(
    tree: ttk.Treeview,
    *,
    max_rows: int = 300,
    padding: int = 24,
    default_max_width: int = 360,
    max_widths: Dict[str, int] | None = None,
) -> None:
    """Resize visible Treeview columns from headers and current row content."""
    try:
        text_font = tkfont.nametofont("TkTextFont")
        heading_font = tkfont.nametofont("TkHeadingFont")
    except Exception:
        text_font = tkfont.Font(font=(UI_FONT_FAMILY, 10))
        heading_font = text_font
    max_widths = max_widths or {}

    rows: list[str] = []

    def collect(parent: str = "") -> None:
        if len(rows) >= max_rows:
            return
        for iid in tree.get_children(parent):
            rows.append(iid)
            if len(rows) >= max_rows:
                return
            collect(iid)

    collect("")

    def bounded_width(column: str, width: int) -> int:
        try:
            minwidth = int(tree.column(column, "minwidth") or 0)
        except Exception:
            minwidth = 0
        if column == "checked":
            minwidth = max(minwidth, 25)
            max_width = max_widths.get(column, 44)
        else:
            minwidth = max(minwidth, 50)
            max_width = max_widths.get(column, default_max_width)
        return max(minwidth, min(width, max_width))

    columns = treeview_display_columns(tree)
    show = str(tree.cget("show") or "")
    if "tree" in show:
        width = heading_font.measure(str(tree.heading("#0", "text") or "")) + padding
        for iid in rows:
            width = max(width, text_font.measure(str(tree.item(iid, "text") or "")) + padding)
        tree.column("#0", width=bounded_width("#0", width))

    for column in columns:
        width = heading_font.measure(str(tree.heading(column, "text") or "")) + padding
        for iid in rows:
            width = max(width, text_font.measure(str(tree.set(iid, column) or "")) + padding)
        tree.column(column, width=bounded_width(column, width))


def treeview_display_columns(tree: ttk.Treeview) -> list[str]:
    try:
        raw_display = tree.cget("displaycolumns")
        all_columns = list(tree["columns"])
        if not raw_display or str(raw_display) == "#all":
            return all_columns
        display_columns = [str(value) for value in tree.tk.splitlist(raw_display)]
        if display_columns == ["#all"]:
            return all_columns
        return display_columns
    except Exception:
        try:
            return list(tree["columns"])
        except Exception:
            return []


def treeview_display_column_name(tree: ttk.Treeview, column_ref: str) -> str:
    if str(column_ref) == "#0":
        return "#0"
    try:
        column_index = int(str(column_ref).lstrip("#")) - 1
    except Exception:
        return ""
    display_columns = treeview_display_columns(tree)
    if 0 <= column_index < len(display_columns):
        return str(display_columns[column_index])
    return ""


def treeview_click_hits_indicator(tree: ttk.Treeview, event: tk.Event) -> bool:
    try:
        element = str(tree.identify("element", event.x, event.y) or "").lower()
    except Exception:
        return False
    return "indicator" in element


def bind_canvas_mousewheel(canvas: tk.Canvas, *widgets: tk.Misc) -> None:
    def on_mousewheel(event: tk.Event) -> str:
        try:
            if getattr(event, "num", None) == 4:
                units = -3
            elif getattr(event, "num", None) == 5:
                units = 3
            else:
                delta = int(getattr(event, "delta", 0) or 0)
                units = -int(delta / 120) if delta else 0
                if units == 0 and delta:
                    units = -1 if delta > 0 else 1
            if units:
                canvas.yview_scroll(units, "units")
        except Exception:
            pass
        return "break"

    targets: list[tk.Misc] = [canvas, *widgets]
    seen: set[str] = set()
    for widget in targets:
        try:
            widget_id = str(widget)
            if widget_id in seen:
                continue
            seen.add(widget_id)
            widget.bind("<MouseWheel>", on_mousewheel, add="+")
            widget.bind("<Button-4>", on_mousewheel, add="+")
            widget.bind("<Button-5>", on_mousewheel, add="+")
        except Exception:
            pass


def set_windows_app_user_model_id() -> None:
    """设置 Windows 任务栏 AppID，让打包后的窗口图标更稳定。"""
    if os.name != "nt":
        return
    try:
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("WaWaCloud.MultiCloudManager")
    except Exception:
        pass


def maximize_window(window: tk.Misc) -> None:
    """Fill the usable desktop area without entering Windows zoomed animation."""
    try:
        window.update_idletasks()  # type: ignore[attr-defined]
    except Exception:
        pass
    if os.name == "nt":
        try:
            class RECT(ctypes.Structure):
                _fields_ = [
                    ("left", ctypes.c_long),
                    ("top", ctypes.c_long),
                    ("right", ctypes.c_long),
                    ("bottom", ctypes.c_long),
                ]

            rect = RECT()
            SPI_GETWORKAREA = 0x0030
            if ctypes.windll.user32.SystemParametersInfoW(SPI_GETWORKAREA, 0, ctypes.byref(rect), 0):
                width = max(800, int(rect.right - rect.left))
                height = max(600, int(rect.bottom - rect.top))
                window.geometry(f"{width}x{height}+{int(rect.left)}+{int(rect.top)}")  # type: ignore[attr-defined]
                return
        except Exception:
            pass
    for method_name in ("state", "wm_state"):
        method = getattr(window, method_name, None)
        if not callable(method):
            continue
        try:
            method("zoomed")
            return
        except Exception:
            pass
    try:
        width = int(window.winfo_screenwidth())  # type: ignore[attr-defined]
        height = int(window.winfo_screenheight())  # type: ignore[attr-defined]
        window.geometry(f"{width}x{height}+0+0")  # type: ignore[attr-defined]
    except Exception:
        pass


def apply_window_icon(window: tk.Misc) -> None:
    """给 Tk 窗口设置 ico 和 png 图标，兼容不同运行环境。"""
    icon_path = resource_path(APP_ICON_FILENAME)
    icon_applied = False
    if os.path.exists(icon_path):
        try:
            window.iconbitmap(icon_path)  # type: ignore[attr-defined]
            icon_applied = True
        except Exception:
            pass
    logo_path = resource_path("logo_app_68.png")
    if os.path.exists(logo_path):
        try:
            photo = tk.PhotoImage(file=logo_path)
            window.iconphoto(True, photo)  # type: ignore[attr-defined]
            setattr(window, "_app_icon_photo_ref", photo)
        except Exception:
            pass


def apply_dialog_icon(window: tk.Misc) -> None:
    """弹窗图标支持按包覆盖；总包用蛙蛙，分包默认用自己的蓝云齿轮。"""
    icon_name = os.environ.get("WAWA_DIALOG_ICON_FILENAME", "app_dialog_icon.ico")
    logo_name = os.environ.get("WAWA_DIALOG_LOGO_FILENAME", "logo_app_68.png")
    icon_path = resource_path(icon_name)
    if os.path.exists(icon_path):
        try:
            window.iconbitmap(icon_path)  # type: ignore[attr-defined]
        except Exception:
            pass
    logo_path = resource_path(logo_name)
    if os.path.exists(logo_path):
        try:
            photo = tk.PhotoImage(file=logo_path)
            window.iconphoto(True, photo)  # type: ignore[attr-defined]
            setattr(window, "_app_dialog_icon_photo_ref", photo)
        except Exception:
            pass


_CLOUD_DIALOG_TITLES = {
    "华为云管理",
    "天翼云管理",
    "火山引擎管理",
}


def _dialog_title_action(message: str) -> str:
    first_line = next((line.strip() for line in str(message or "").splitlines() if line.strip()), "")
    if not first_line:
        return ""
    patterns = (
        ("确认开始购买并搭建", "购买并搭建"),
        ("开始购买并搭建", "购买并搭建"),
        ("购买并搭建任务结束", "购买并搭建"),
        ("购买并搭建任务完成", "购买并搭建"),
        ("购买并搭建选择地区", "购买并搭建选择地区"),
        ("确认开始购买选择地区", "购买选择地区"),
        ("购买选择地区", "购买选择地区"),
        ("购买任务结束", "购买"),
        ("确认开始搭建选择地区", "搭建选择地区"),
        ("搭建选择地区", "搭建选择地区"),
        ("确认开始搭建", "搭建"),
        ("开始搭建", "搭建"),
        ("搭建任务结束", "搭建"),
        ("搭建任务完成", "搭建"),
        ("查询资源任务未完成", "资源查询"),
        ("查询资源任务失败", "资源查询"),
        ("查询资源任务结束", "资源查询"),
        ("查询资源任务完成", "资源查询"),
        ("读取账号资源", "资源查询"),
        ("账号资源读取", "资源查询"),
        ("确认重装", "重装云服务器"),
        ("重装云服务器", "重装云服务器"),
        ("重装系统", "重装云服务器"),
        ("确认退订", "退订/删除资源"),
        ("退订/删除/释放资源", "退订/删除资源"),
        ("退订/删除选定资源", "退订/删除资源"),
        ("退订删除资源", "退订/删除资源"),
        ("资源处理完成", "退订/删除资源"),
        ("清空账号资源", "清空账号资源"),
        ("一键清空账号资源", "清空账号资源"),
        ("确认更换新IP", "更换新IP"),
        ("更换新IP完成", "更换新IP"),
        ("更换新IP", "更换新IP"),
        ("确认更换选择地区IP", "更换选择地区IP"),
        ("更换选择地区IP完成", "更换选择地区IP"),
        ("更换选择地区IP", "更换选择地区IP"),
        ("日志压缩包已导出", "导出日志"),
        ("当前没有 L2TP", "复制结果"),
        ("当前没有 Socks5", "复制结果"),
        ("当前没有纯 Socks5", "复制结果"),
        ("L2TP 输出已复制", "复制结果"),
        ("Socks5 输出已复制", "复制结果"),
        ("纯 Socks5 输出已复制", "复制结果"),
        ("L2TP 结果已复制", "复制结果"),
        ("Socks5 结果已复制", "复制结果"),
        ("纯 Socks5 结果已复制", "复制结果"),
        ("当前已有任务正在执行", "延时队列"),
        ("已加入延时执行队列", "延时队列"),
        ("队列任务执行完毕", "延时队列"),
    )
    for marker, suffix in patterns:
        if first_line.startswith(marker) or marker in first_line:
            return suffix
    if "AK/SK" in first_line or "合作伙伴" in first_line or "客户归属" in first_line:
        return "AK/SK校验"
    if "授权码" in first_line:
        return "授权码校验"
    if "完成" in first_line or "成功" in first_line:
        return "任务结果"
    if "失败" in first_line or "错误" in first_line:
        return "错误提示"
    return ""


def _dialog_title_kind(default_suffix: str) -> str:
    suffix = str(default_suffix or "").strip()
    if "确认" in suffix:
        return "确认"
    return "提醒"


def cloud_dialog_title(title: str, message: str = "", *, default_suffix: str = "提示") -> str:
    base = str(title or "").strip()
    if "-" in base or base not in _CLOUD_DIALOG_TITLES:
        return base or default_suffix
    action = _dialog_title_action(message).strip()
    kind = _dialog_title_kind(default_suffix)
    if action:
        return f"{base}-{action}-{kind}"
    return f"{base}-操作提示-{kind}" if kind else base


def _center_transient_dialog(dialog: tk.Toplevel, parent: tk.Misc, min_width: int = 360, min_height: int = 140) -> None:
    dialog.update_idletasks()
    parent_top = parent.winfo_toplevel()
    width = max(min_width, dialog.winfo_reqwidth())
    height = max(min_height, dialog.winfo_reqheight())
    try:
        x = parent_top.winfo_rootx() + max(0, (parent_top.winfo_width() - width) // 2)
        y = parent_top.winfo_rooty() + max(0, (parent_top.winfo_height() - height) // 2)
    except tk.TclError:
        x = y = 120
    dialog.geometry(f"{width}x{height}+{x}+{y}")


_DIALOG_SECTION_SEPARATOR = "────────────────────────"


APP_DIALOG_BUTTON_STYLE = "AppDialog.TButton"
APP_DIALOG_BUTTON_MIN_WIDTH = 12
APP_DIALOG_BUTTON_MAX_WIDTH = 34


def dialog_button_width(label: str, *, min_width: int = APP_DIALOG_BUTTON_MIN_WIDTH, max_width: int = APP_DIALOG_BUTTON_MAX_WIDTH) -> int:
    display_units = 0
    for char in str(label or ""):
        display_units += 2 if ord(char) > 127 else 1
    return max(min_width, min(max_width, display_units + 4))


def dialog_button_style(widget: tk.Misc) -> str:
    try:
        style = ttk.Style(widget)
        style.configure(APP_DIALOG_BUTTON_STYLE, padding=(10, 6))
    except Exception:
        pass
    return APP_DIALOG_BUTTON_STYLE


def format_app_dialog_message(message: str) -> str:
    text = str(message or "").strip()
    if not text or _DIALOG_SECTION_SEPARATOR in text:
        return text
    sections = [part.strip() for part in re.split(r"\n\s*\n", text) if part.strip()]
    if len(sections) <= 1:
        return text
    queue_summary = "队列任务执行完毕" in sections[0]
    secondary_markers = (
        "当前任务：",
        "后续任务：",
        "确认后将加入延时执行队列",
        "另外当前已有任务正在执行",
    )
    formatted = [sections[0]]
    for section in sections[1:]:
        first_line = next((line.strip() for line in section.splitlines() if line.strip()), "")
        separator = queue_summary or any(marker in first_line for marker in secondary_markers)
        joiner = f"\n\n{_DIALOG_SECTION_SEPARATOR}\n\n" if separator else "\n\n"
        formatted.append(joiner + section)
    return "".join(formatted)


def show_app_message_dialog(
    parent: tk.Misc,
    title: str,
    message: str,
    *,
    modal: bool = False,
    existing_dialog: tk.Toplevel | None = None,
    actions: Sequence[Tuple[str, Callable[[], None]]] | None = None,
) -> tk.Toplevel:
    """Show or update the app's standard message dialog."""
    text = format_app_dialog_message(message)
    parent_top = parent.winfo_toplevel()
    action_items = tuple(actions or ())
    action_labels = tuple(label for label, _callback in action_items)
    if existing_dialog is not None:
        try:
            if existing_dialog.winfo_exists():
                existing_labels = tuple(getattr(existing_dialog, "_app_action_labels", ()))
                if existing_labels != action_labels:
                    existing_dialog.destroy()
                else:
                    message_var = getattr(existing_dialog, "_app_message_var", None)
                    if isinstance(message_var, tk.StringVar):
                        message_var.set(text)
                    existing_dialog.deiconify()
                    existing_dialog.title(cloud_dialog_title(title, text, default_suffix="提示"))
                    apply_dialog_icon(existing_dialog)
                    existing_dialog.lift(parent_top)
                    existing_dialog.focus_force()
                    _center_transient_dialog(existing_dialog, parent_top, min_width=320, min_height=120)
                    return existing_dialog
        except tk.TclError:
            pass

    dialog = tk.Toplevel(parent_top)
    dialog.title(cloud_dialog_title(title, text, default_suffix="提示"))
    dialog.resizable(False, False)
    dialog.transient(parent_top)
    apply_dialog_icon(dialog)

    message_var = tk.StringVar(value=text)
    setattr(dialog, "_app_message_var", message_var)
    setattr(dialog, "_app_action_labels", action_labels)

    frame = ttk.Frame(dialog, padding=(20, 16, 20, 18))
    frame.pack(fill=tk.BOTH, expand=True)
    ttk.Label(
        frame,
        textvariable=message_var,
        wraplength=420,
        justify=tk.LEFT,
        font=(UI_FONT_FAMILY, 11),
    ).pack(fill=tk.X, pady=(0, 14))
    buttons = ttk.Frame(frame)
    buttons.pack(anchor="center", pady=(2, 0))
    button_style = dialog_button_style(buttons)

    def close() -> None:
        try:
            dialog.destroy()
        except tk.TclError:
            pass

    if action_items:
        for index, (label, callback) in enumerate(action_items):
            def run_action(action: Callable[[], None] = callback) -> None:
                close()
                action()

            ttk.Button(buttons, text=label, command=run_action, width=dialog_button_width(label), style=button_style).grid(
                row=0,
                column=index,
                sticky="nsew",
                padx=(0, 10) if index < len(action_items) - 1 else 0,
                ipady=2,
            )
    else:
        ttk.Button(buttons, text="确认", command=close, width=dialog_button_width("确认"), style=button_style).pack(side=tk.LEFT, ipady=2)
    dialog.protocol("WM_DELETE_WINDOW", close)
    _center_transient_dialog(dialog, parent_top, min_width=320, min_height=120)
    dialog.focus_force()
    if modal:
        dialog.grab_set()
        dialog.wait_window()
    return dialog


def ask_app_confirm_dialog(
    parent: tk.Misc,
    title: str,
    message: str,
    *,
    ok_text: str = "确认",
    cancel_text: str = "取消",
) -> bool:
    """Show the app's standard confirm/cancel dialog."""
    parent_top = parent.winfo_toplevel()
    text = format_app_dialog_message(message)
    dialog = tk.Toplevel(parent_top)
    dialog.title(cloud_dialog_title(title, text, default_suffix="确认"))
    dialog.resizable(False, False)
    dialog.transient(parent_top)
    apply_dialog_icon(dialog)
    result = {"ok": False}

    frame = ttk.Frame(dialog, padding=(22, 22, 22, 30))
    frame.pack(fill=tk.BOTH, expand=True)
    ttk.Label(frame, text=text, wraplength=560, justify=tk.LEFT).pack(fill=tk.X, pady=(0, 30))
    buttons = ttk.Frame(frame)
    buttons.pack(anchor="center", pady=(10, 0))
    button_style = dialog_button_style(buttons)

    def confirm() -> None:
        result["ok"] = True
        dialog.destroy()

    def cancel() -> None:
        dialog.destroy()

    ttk.Button(buttons, text=ok_text, command=confirm, width=dialog_button_width(ok_text), style=button_style).pack(side=tk.LEFT, padx=(0, 14), ipady=2)
    ttk.Button(buttons, text=cancel_text, command=cancel, width=dialog_button_width(cancel_text), style=button_style).pack(side=tk.LEFT, ipady=2)
    dialog.protocol("WM_DELETE_WINDOW", cancel)
    _center_transient_dialog(dialog, parent_top, min_width=440, min_height=200)
    dialog.grab_set()
    dialog.focus_force()
    parent_top.wait_window(dialog)
    return bool(result["ok"])


def make_text_user_readonly(text: tk.Text) -> tk.Text:
    """Keep programmatic log updates working while blocking user edits."""

    def block_keypress(event: tk.Event) -> str | None:
        keysym = str(getattr(event, "keysym", "") or "")
        state = int(getattr(event, "state", 0) or 0)
        ctrl_pressed = bool(state & 0x0004)
        if ctrl_pressed and keysym.lower() == "a":
            text.tag_add(tk.SEL, "1.0", tk.END)
            text.mark_set(tk.INSERT, "1.0")
            text.see(tk.INSERT)
            return "break"
        if ctrl_pressed and keysym.lower() in {"c", "insert"}:
            return None
        if keysym in READONLY_TEXT_ALLOWED_KEYSYMS:
            return None
        return "break"

    text.bind("<KeyPress>", block_keypress, add="+")
    text.bind("<<Paste>>", lambda _event: "break", add="+")
    text.bind("<<Cut>>", lambda _event: "break", add="+")
    text.bind("<Button-2>", lambda _event: "break", add="+")
    return text


def create_fixed_tab_pages(
    parent: tk.Misc,
    tabs: Sequence[Tuple[str, str]],
    *,
    page_padding: int = 0,
) -> Dict[str, ttk.Frame]:
    """创建固定尺寸标签按钮和对应页面，避免切换时布局抖动。"""
    parent.columnconfigure(0, weight=1)  # type: ignore[attr-defined]
    parent.rowconfigure(0, weight=0)  # type: ignore[attr-defined]
    parent.rowconfigure(1, weight=1)  # type: ignore[attr-defined]
    tab_bar = tk.Frame(parent, bg=CONTENT_BG)
    tab_bar.grid(row=0, column=0, sticky="w")
    host = ttk.Frame(parent)
    host.grid(row=1, column=0, sticky="nsew")
    host.columnconfigure(0, weight=1)
    host.rowconfigure(0, weight=1)

    pages: Dict[str, ttk.Frame] = {}
    buttons: Dict[str, tk.Label] = {}
    underlines: Dict[str, tk.Frame] = {}

    def activate(active_key: str) -> None:
        for key in pages:
            active = key == active_key
            button = buttons[key]
            underline = underlines[key]
            button.configure(
                bg=ACCENT_BG if active else CONTENT_BG,
                fg=TEXT_FG,
            )
            underline.configure(bg=TEXT_FG if active else CONTENT_BG)
        pages[active_key].tkraise()

    for idx, (key, label) in enumerate(tabs):
        holder = tk.Frame(
            tab_bar,
            width=TAB_PIXEL_WIDTH,
            height=TAB_PIXEL_HEIGHT,
            bg=BORDER_COLOR,
            bd=0,
            highlightthickness=0,
        )
        holder.grid(row=0, column=idx, sticky="w", padx=(0 if idx == 0 else 6, 0), pady=(0, 6))
        holder.grid_propagate(False)
        holder.pack_propagate(False)
        holder.columnconfigure(0, weight=1)
        holder.rowconfigure(0, weight=1)
        holder.rowconfigure(1, weight=0)
        holder.rowconfigure(2, weight=0)
        button_font = (UI_LARGE_FONT_FAMILY, 14, "bold")
        button = tk.Label(
            holder,
            text=label,
            bg=CONTENT_BG,
            fg=TEXT_FG,
            bd=0,
            font=button_font,
            cursor="hand2",
            anchor="center",
        )
        button.place(x=1, y=1, width=max(1, TAB_PIXEL_WIDTH - 2), height=max(1, TAB_PIXEL_HEIGHT - 2))
        button.bind("<Button-1>", lambda _event, item_key=key: activate(item_key))
        holder.bind("<Button-1>", lambda _event, item_key=key: activate(item_key))
        try:
            underline_width = max(28, tkfont.Font(font=button_font).measure(label) + 10)
        except Exception:
            underline_width = max(28, len(label) * 14 + 10)
        underline = tk.Frame(button, width=underline_width, height=1, bg=CONTENT_BG)
        underline.place(relx=0.5, y=max(0, TAB_PIXEL_HEIGHT - 11), anchor="n", width=underline_width, height=1)
        underline.bind("<Button-1>", lambda _event, item_key=key: activate(item_key))
        underline.lift()
        page = ttk.Frame(host, padding=page_padding)
        page.grid(row=0, column=0, sticky="nsew")
        pages[key] = page
        buttons[key] = button
        underlines[key] = underline

    if tabs:
        activate(tabs[0][0])
    return pages


def build_realtime_log_panel(
    parent: tk.Misc,
    *,
    export_command,
    stop_command,
    title: str = "实时日志",
    text_factory=None,
    text_kwargs: dict | None = None,
    stop_state: str = "normal",
) -> tuple[tk.Text, ttk.Button, ttk.Button]:
    """创建统一的实时日志面板：日志框在上，操作按钮固定在下方。"""
    parent.columnconfigure(0, weight=1)  # type: ignore[attr-defined]
    parent.rowconfigure(0, weight=1)  # type: ignore[attr-defined]
    frame = ttk.LabelFrame(parent, text=title, padding=LOG_PANEL_PADDING)
    frame.grid(row=0, column=0, sticky="nsew")
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(0, weight=1)

    kwargs = {
        "wrap": "word",
        "width": LOG_TEXT_WIDTH,
        "height": LOG_TEXT_HEIGHT,
        "font": (UI_FONT_FAMILY, 10),
        "bg": TEXT_BG,
        "fg": TEXT_FG,
        "insertbackground": TEXT_FG,
    }
    if text_kwargs:
        kwargs.update(text_kwargs)
    kwargs["font"] = (UI_FONT_FAMILY, 10)
    factory = text_factory or tk.Text
    text = factory(frame, **kwargs)
    if isinstance(text, tk.Text):
        make_text_user_readonly(text)
    text.grid(row=0, column=0, sticky="nsew")
    scrollbar = ttk.Scrollbar(frame, command=text.yview, style="App.Vertical.TScrollbar")
    scrollbar.grid(row=0, column=1, sticky="ns")
    text.configure(yscrollcommand=scrollbar.set)

    actions = ttk.Frame(frame)
    actions.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(8, 0))
    actions.columnconfigure(0, weight=1)
    export_button = ttk.Button(actions, text="导出日志", width=LOG_ACTION_BUTTON_WIDTH, command=export_command)
    stop_button = ttk.Button(actions, text="停止任务", width=LOG_ACTION_BUTTON_WIDTH, command=stop_command, state=stop_state)
    export_button.grid(row=0, column=0, sticky="w")
    stop_button.grid(row=0, column=1, sticky="e")
    return text, export_button, stop_button


def build_result_output_panel(
    parent: tk.Misc,
    *,
    copy_l2tp_command,
    copy_plain_socks_command,
    copy_socks_command,
    text_factory=None,
    text_kwargs: dict | None = None,
) -> tuple[tk.Text, tk.Text]:
    """创建统一的 L2TP 和 Socks5 结果框，保证各云大小和按钮位置一致。"""
    parent.columnconfigure(0, weight=1)  # type: ignore[attr-defined]
    parent.columnconfigure(1, weight=1)  # type: ignore[attr-defined]
    parent.rowconfigure(0, weight=1)  # type: ignore[attr-defined]

    factory = text_factory or tk.Text
    kwargs = {
        "wrap": "word",
        "height": RESULT_TEXT_HEIGHT,
        "bg": TEXT_BG,
        "fg": TEXT_FG,
        "insertbackground": TEXT_FG,
    }
    if text_kwargs:
        kwargs.update(text_kwargs)

    l2tp_frame = ttk.LabelFrame(parent, text="L2TP", padding=LOG_PANEL_PADDING)
    socks_frame = ttk.LabelFrame(parent, text="Socks5", padding=LOG_PANEL_PADDING)
    l2tp_frame.grid(row=0, column=0, sticky="nsew", padx=(0, LOG_PANEL_PADDING))
    socks_frame.grid(row=0, column=1, sticky="nsew", padx=(LOG_PANEL_PADDING, 0))
    for frame in (l2tp_frame, socks_frame):
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(1, weight=1)

    l2tp_head = ttk.Frame(l2tp_frame)
    l2tp_head.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 4))
    l2tp_head.columnconfigure(0, weight=1)
    ttk.Button(l2tp_head, text="复制L2TP", width=RESULT_COPY_BUTTON_WIDTH, command=copy_l2tp_command).grid(row=0, column=1, sticky="e")

    socks_head = ttk.Frame(socks_frame)
    socks_head.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 4))
    socks_head.columnconfigure(0, weight=1)
    ttk.Button(socks_head, text="复制纯Socks5", width=RESULT_COPY_BUTTON_WIDTH, command=copy_plain_socks_command).grid(row=0, column=1, sticky="e", padx=(0, 6))
    ttk.Button(socks_head, text="复制Socks5", width=RESULT_COPY_BUTTON_WIDTH, command=copy_socks_command).grid(row=0, column=2, sticky="e")

    l2tp_text = factory(l2tp_frame, **kwargs)
    socks_text = factory(socks_frame, **kwargs)
    if isinstance(l2tp_text, tk.Text):
        make_text_user_readonly(l2tp_text)
    if isinstance(socks_text, tk.Text):
        make_text_user_readonly(socks_text)
    l2tp_text.grid(row=1, column=0, sticky="nsew")
    socks_text.grid(row=1, column=0, sticky="nsew")
    l2tp_scroll = ttk.Scrollbar(l2tp_frame, command=l2tp_text.yview, style="App.Vertical.TScrollbar")
    socks_scroll = ttk.Scrollbar(socks_frame, command=socks_text.yview, style="App.Vertical.TScrollbar")
    l2tp_scroll.grid(row=1, column=1, sticky="ns")
    socks_scroll.grid(row=1, column=1, sticky="ns")
    l2tp_text.configure(yscrollcommand=l2tp_scroll.set)
    socks_text.configure(yscrollcommand=socks_scroll.set)
    return l2tp_text, socks_text


