import sys
import os
import json
import threading
import socket
import struct
import time as _time
import datetime
import urllib.request
import webbrowser
import qtawesome as qta
from PyQt6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QHBoxLayout,
    QScrollArea,
    QPushButton,
    QApplication,
    QMenu,
    QSystemTrayIcon,
    QLineEdit,
    QLabel,
    QMessageBox,
    QGridLayout,
)
from PyQt6.QtCore import (
    Qt,
    QPoint,
    QPropertyAnimation,
    QEasingCurve,
    QTimer,
    QEvent,
    QSize,
    pyqtSignal,
)
from PyQt6.QtGui import QPixmap, QPainter, QColor, QBrush, QPen, QIcon, QCursor
from db import database
from ui.group import GroupWidget
import ui.theme as theme
import ui.dialog_style as dialog_style
from ui.updater import Updater

PANEL_W = 260
PANEL_H = 40
MIN_W, MIN_H = 400, 5
ANIM_MS = 180
RESIZE_M = 8


class WeatherBox(QWidget):
    """顶部天气信息模块（重写：大图标 + 左侧文案 + 右侧温度布局）"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self._weather_dir = os.path.join(
            os.path.dirname(os.path.dirname(__file__)), "assets", "imgs", "weather"
        )

        # 顶部栏高度由 PanelWindow 强制对齐（weather_bar = 80）
        self.setFixedHeight(80)
        # 控制宽度，避免把右侧“网络时间”挤出
        self.setFixedWidth(170)

        # 左侧：图标（大）+ 天气名称（小一点）
        self._left_widget = QWidget()
        self._left_widget.setFixedWidth(68)
        left_lay = QVBoxLayout(self._left_widget)
        left_lay.setContentsMargins(0, 0, 0, 0)
        left_lay.setSpacing(6)

        self._icon_label = QLabel()
        self._icon_label.setFixedSize(70, 70)
        self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self._icon_label.setStyleSheet("background:transparent;")

        self._weather_name_label = QLabel("")
        self._weather_name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self._weather_name_label.setStyleSheet(
            "font-size:12px; font-weight:700; background:transparent;"
        )
        self._weather_name_label.setFixedWidth(self._left_widget.width())

        left_lay.addWidget(self._icon_label)
        left_lay.addWidget(self._weather_name_label)

        # 右侧：温度（大）+ 地区 + 风力
        self._right_widget = QWidget()
        self._right_widget.setFixedWidth(102)
        right_lay = QVBoxLayout(self._right_widget)
        right_lay.setContentsMargins(0, 0, 0, 0)
        right_lay.setSpacing(4)

        self._temp_label = QLabel("--°C")
        self._temp_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
        self._temp_label.setFixedWidth(self._right_widget.width())
        self._temp_label.setStyleSheet(
            "font-weight:900; font-size:28px; background:transparent; padding:0px; margin:0px;"
        )

        self._location_label = QLabel("")
        self._location_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
        self._location_label.setFixedWidth(self._right_widget.width())
        self._location_label.setStyleSheet(
            "font-size:12px; font-weight:700; background:transparent;"
        )

        self._wind_label = QLabel("")
        self._wind_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
        self._wind_label.setFixedWidth(self._right_widget.width())
        self._wind_label.setStyleSheet(
            "font-size:12px; background:transparent;"
        )

        right_lay.addWidget(self._temp_label)
        right_lay.addWidget(self._location_label)
        right_lay.addWidget(self._wind_label)

        lay = QHBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(6)
        lay.addWidget(self._left_widget)
        lay.addWidget(self._right_widget)

        self._last_tooltip: str | None = None

    def set_theme_colors(self, color_fg: str):
        self._temp_label.setStyleSheet(
            f"font-weight:900; font-size:28px; color:{color_fg}; background:transparent; padding:0px; margin:0px;"
        )
        self._weather_name_label.setStyleSheet(
            f"font-size:12px; font-weight:700; color:{color_fg}; background:transparent;"
        )
        self._location_label.setStyleSheet(
            f"font-size:12px; font-weight:700; color:{color_fg}; background:transparent;"
        )
        self._wind_label.setStyleSheet(
            f"font-size:12px; color:{color_fg}; background:transparent;"
        )

    def _set_elided(self, label: QLabel, text: str):
        text = text or ""
        # 统一右侧省略，避免撑爆布局
        fm = label.fontMetrics()
        label.setText(
            fm.elidedText(text, Qt.TextElideMode.ElideRight, max(10, label.width()))
        )

    def _make_weather_icon(self, weather: str) -> QPixmap:
        wname = (weather or "").strip()
        img_path = os.path.join(self._weather_dir, f"{wname}.png")
        if os.path.exists(img_path):
            pm = QPixmap(img_path)
            if not pm.isNull():
                target = self._icon_label.size()
                return pm.scaled(
                    target.width(),
                    target.height(),
                    Qt.AspectRatioMode.KeepAspectRatio,
                    Qt.TransformationMode.SmoothTransformation,
                )
        # fallback：透明背景的占位圆
        pm = QPixmap(self._icon_label.size())
        pm.fill(Qt.GlobalColor.transparent)
        p = QPainter(pm)
        p.setRenderHint(QPainter.RenderHint.Antialiasing)
        p.setPen(QPen(QColor("#888888"), 2))
        p.drawEllipse(6, 6, pm.width() - 12, pm.height() - 12)
        p.end()
        return pm

    def update_weather(self, data: dict):
        if not data:
            return

        temp = data.get("temperature", "--")
        weather = str(data.get("weather", "") or "")
        province = str(data.get("province", "") or "")
        city = str(data.get("city", "") or "")
        district = str(data.get("district", "") or "")
        wind_direction = str(data.get("wind_direction", "") or "")
        wind_power = str(data.get("wind_power", "") or "")

        # 地点：优先 city，其次 province；如有 district 追加
        location = city or province or "--"
        if district and district not in location:
            location = f"{location}{district}"

        # 温度：尽量显示为整数（接口一般是数字）
        if temp in ("", None):
            temp_text = "--°C"
        else:
            try:
                temp_text = f"{int(float(temp))}°C"
            except Exception:
                temp_text = f"{temp}°C"

        self._temp_label.setText(temp_text)
        self._icon_label.setPixmap(self._make_weather_icon(weather))

        self._set_elided(self._weather_name_label, weather)
        self._set_elided(self._location_label, location)
        self._set_elided(self._wind_label, f"{wind_direction}{wind_power}".replace(" ", ""))

    def show_loading(self):
        self._temp_label.setText("--°C")
        self._icon_label.setPixmap(QPixmap(self._icon_label.size()))
        self._weather_name_label.setText("获取中…")
        self._location_label.setText("")
        self._wind_label.setText("")
        self._last_tooltip = None


def _make_tray_icon():
    logo = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
    if os.path.exists(logo):
        return QIcon(logo)
    px = QPixmap(32, 32)
    px.fill(Qt.GlobalColor.transparent)
    p = QPainter(px)
    p.setRenderHint(QPainter.RenderHint.Antialiasing)
    p.setBrush(QBrush(QColor("#4a9eff")))
    p.setPen(Qt.PenStyle.NoPen)
    p.drawRoundedRect(2, 2, 28, 28, 6, 6)
    p.setBrush(QBrush(QColor("white")))
    for y in [9, 15, 21]:
        p.drawRoundedRect(7, y, 18, 3, 1, 1)
    p.end()
    return QIcon(px)


class GroupsContainer(QWidget):
    """支持分组拖拽排序的容器"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAcceptDrops(True)
        self._indicator_pos = -1  # 指示线位置（像素 y）

    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat("application/x-group-id"):
            event.acceptProposedAction()
        else:
            event.ignore()

    def dragMoveEvent(self, event):
        if event.mimeData().hasFormat("application/x-group-id"):
            self._indicator_pos = event.position().toPoint().y()
            self.update()
            event.acceptProposedAction()

    def dragLeaveEvent(self, event):
        self._indicator_pos = -1
        self.update()

    def dropEvent(self, event):
        self._indicator_pos = -1
        self.update()
        if not event.mimeData().hasFormat("application/x-group-id"):
            event.ignore()
            return

        gid = int(event.mimeData().data("application/x-group-id").data().decode())
        drop_y = event.position().toPoint().y()

        # 找到插入位置
        layout = self.layout()
        insert_idx = layout.count() - 1  # 默认插到最后（stretch 前）
        for i in range(layout.count() - 1):
            item = layout.itemAt(i)
            if item and item.widget():
                geo = item.widget().geometry()
                if drop_y < geo.center().y():
                    insert_idx = i
                    break

        # 收集当前顺序，把拖动的移到 insert_idx
        ids = []
        for i in range(layout.count() - 1):
            item = layout.itemAt(i)
            if item and item.widget():
                ids.append(item.widget().group_data["id"])

        if gid in ids:
            ids.remove(gid)
            ids.insert(min(insert_idx, len(ids)), gid)
            from db import database

            database.reorder_groups(ids)
            # 通知父级刷新
            p = self.parent()
            while p:
                if hasattr(p, "refresh_groups"):
                    p.refresh_groups()
                    break
                p = p.parent()

        event.acceptProposedAction()

    def paintEvent(self, event):
        super().paintEvent(event)
        if self._indicator_pos < 0:
            return
        p = QPainter(self)
        p.setPen(QPen(QColor("#4a9eff"), 2))
        p.drawLine(0, self._indicator_pos, self.width(), self._indicator_pos)
        p.end()


class PanelWindow(QWidget):
    weather_updated = pyqtSignal(dict)
    # base_utc_ts: 接口返回的 UTC 时间戳（秒）
    # tz_offset_sec: 目标时区相对 UTC 的偏移（秒）
    time_synced = pyqtSignal(float, int)

    def __init__(self):
        super().__init__()
        self.setWindowTitle("牛马软件柜")
        # 默认不置顶；仅图钉开启时与悬浮球一并置顶（见 _apply_pin_window_layer）
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        # 任务栏/开始菜单图标：使用项目 logo.png
        logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
        if os.path.exists(logo_path):
            self.setWindowIcon(QIcon(logo_path))
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        self.setAcceptDrops(True)
        self.setMinimumSize(MIN_W, MIN_H)
        self.resize(PANEL_W, PANEL_H)
        self._persist_w = PANEL_W
        self._persist_h = PANEL_H
        self._screen_hooked = False
        # 让窗口本身也追踪鼠标（不按键也收到 mouseMoveEvent）
        self.setMouseTracking(True)

        self._anim = None
        self._win_drag_pos = None
        self._resizing = False
        self._resize_edge = None
        self._resize_start_global = None
        self._resize_start_geo = None
        self._pinned = False

        self._build_ui()
        self._setup_tray()
        self._apply_theme()
        self._restore_geometry()
        self.weather_updated.connect(self._on_weather_updated)
        self.time_synced.connect(self._on_time_synced)

        self._weather_fetching = False
        self._time_base_utc_ts: float = 0.0
        self._time_mono_base: float = 0.0
        # 默认按中国时区（大概率与你的 IP 定位一致）；后续会在天气接口返回后修正
        self._tz_offset_sec: int = 8 * 3600
        self._time_timer = QTimer(self)
        self._time_timer.setInterval(1000)
        self._time_timer.timeout.connect(self._refresh_time_label)

        self._updater = Updater(self)

    def showEvent(self, event):
        super().showEvent(event)
        if not self._screen_hooked:
            wh = self.windowHandle()
            if wh is not None:
                wh.screenChanged.connect(self._on_screen_changed)
                self._screen_hooked = True
        # 首次显示时异步获取一次天气
        if getattr(self, "_weather_once", False) is False:
            self._weather_once = True
            self._update_weather_async()

        if getattr(self, "_time_once", False) is False:
            self._time_once = True
            self._update_time_from_ip_async()

    def _on_screen_changed(self, screen=None):
        """跨不同 DPI 的显示器拖动时，Qt 可能改变窗口逻辑尺寸，按记录值拉回。"""
        if getattr(self, "_body", None) is not None and not self._body.isVisible():
            return
        w = getattr(self, "_persist_w", PANEL_W)
        h = getattr(self, "_persist_h", PANEL_H)
        if w >= MIN_W and h >= MIN_H:
            self.resize(w, h)

    # ── 天气 ─────────────────────────────────────────────
    def _update_weather_async(self):
        def _worker():
            data: dict | None = None
            try:
                url = "https://uapis.cn/api/v1/misc/weather"
                req = urllib.request.Request(
                    url,
                    headers={
                        "User-Agent": "Mozilla/5.0",
                        "Accept": "application/json",
                    },
                )
                with urllib.request.urlopen(req, timeout=5) as resp:
                    raw = resp.read().decode("utf-8", "replace").strip()
                    if raw:
                        data = json.loads(raw)
                        if not isinstance(data, dict):
                            data = None
            except Exception as e:
                # 方便你在控制台看到原因
                print("Weather fetch failed:", repr(e))
                print("Weather raw (if any):", repr(locals().get("raw", ""))[:200])
                data = None

            # 线程内不要直接操作 QWidget，改为发信号
            self.weather_updated.emit(data or {})

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

    def _on_weather_updated(self, data: dict):
        if not hasattr(self, "weather_box"):
            return
        self.weather_box.update_weather(data)
        self._weather_fetching = False
        # 用 weather 接口返回的 adcode（由 IP 自动定位）推断时区偏移
        # 目前你的接口数据基本为中国区（你给的示例 adcode=320706），中国全时区统一为 UTC+8
        try:
            adcode = data.get("adcode", "")
            ad_str = str(adcode).strip()
            if ad_str.isdigit() and len(ad_str) == 6:
                self._tz_offset_sec = 8 * 3600
            else:
                self._tz_offset_sec = 0
        except Exception:
            self._tz_offset_sec = 8 * 3600
        if self._time_base_utc_ts > 0:
            self._refresh_time_label()

    def _refresh_weather(self):
        if getattr(self, "_weather_fetching", False):
            return
        self._weather_fetching = True
        try:
            self.weather_box.show_loading()
        except Exception:
            pass
        self._update_weather_async()

    def _refresh_time_label(self):
        if not hasattr(self, "time_label"):
            return
        if self._time_base_utc_ts <= 0:
            return
        # 用接口返回的 UTC 基准时间 + monotonic 增量（不受本机时间影响）
        delta_sec = _time.monotonic() - self._time_mono_base
        utc_now = self._time_base_utc_ts + delta_sec
        local_now = utc_now + self._tz_offset_sec
        dt = datetime.datetime.utcfromtimestamp(local_now)
        self.time_label.setText(dt.strftime("%H:%M:%S"))
        self.time_date_label.setText(dt.strftime("%Y-%m-%d"))

    def _update_time_from_ip_async(self):
        def _ntp_query(server: str, timeout: float) -> float:
            # ... (保持原有的 ntp_query 逻辑不变) ...
            msg = b"\x1b" + 47 * b"\0"
            with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
                s.settimeout(timeout)
                s.sendto(msg, (server, 123))
                data, _ = s.recvfrom(512)
            t = struct.unpack("!II", data[40:48])
            return t[0] + t[1] / 2**32 - 2208988800.0

        def _worker():
            # 这里的服务器列表是解决问题的核心：优先国内阿里、腾讯节点
            servers = [
                "ntp.aliyun.com",
                "ntp.tencent.com",
                "cn.pool.ntp.org",
                "pool.ntp.org",
            ]
            base_utc_ts = 0.0

            for srv in servers:
                try:
                    # 尝试连接，超时时间设为 2.5 秒
                    base_utc_ts = float(_ntp_query(srv, timeout=2.5))
                    if base_utc_ts > 0:
                        break  # 只要有一个成功，就跳出循环
                except Exception:
                    continue  # 失败了换下一个服务器

            # 兜底：如果所有服务器都连不上，回退到本机时间
            if base_utc_ts <= 0:
                base_utc_ts = _time.time()

            tz_offset_sec = int(getattr(self, "_tz_offset_sec", 8 * 3600))
            self.time_synced.emit(base_utc_ts, tz_offset_sec)

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

    def _on_time_synced(self, base_utc_ts: float, tz_offset_sec: int):
        self._time_base_utc_ts = base_utc_ts
        self._tz_offset_sec = tz_offset_sec
        self._time_mono_base = _time.monotonic()
        self._refresh_time_label()
        if not self._time_timer.isActive():
            self._time_timer.start()

    # ── UI ──────────────────────────────────────────────
    def _get_quick_actions(self):
        """返回快捷动作列表，悬浮球右键菜单和左侧快捷栏共用同一份。
        每项: (tooltip, qtawesome_icon, callback)
        排除重启/退出。"""
        return [
            ("管理员运行 CMD",        "fa5s.terminal",  self._open_admin_cmd),
            ("管理员运行 PowerShell", "fa5b.windows",   self._open_admin_powershell),
            ("打开默认浏览器",         "fa5s.globe",     self._open_default_browser),
        ]

    def _build_ui(self):
        root = QVBoxLayout(self)
        root.setContentsMargins(0, 0, 0, 0)

        self.container = QWidget()
        self.container.setObjectName("container")
        self.container.setMouseTracking(True)

        # 横向：左侧快捷栏 + 右侧主内容
        h_layout = QHBoxLayout(self.container)
        h_layout.setContentsMargins(0, 0, 0, 0)
        h_layout.setSpacing(0)

        # ── 左侧快捷栏
        self._quick_bar = QWidget()
        self._quick_bar.setObjectName("quick_bar")
        self._quick_bar.setFixedWidth(50)
        quick_layout = QVBoxLayout(self._quick_bar)
        quick_layout.setContentsMargins(0, 10, 0, 10)
        quick_layout.setSpacing(6)
        quick_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter)

        self._quick_btns = []
        for tooltip, icon_name, callback in self._get_quick_actions():
            btn = QPushButton()
            btn.setFixedSize(34, 34)
            btn.setToolTip(tooltip)
            btn.setStyleSheet("border:none; background:transparent; border-radius:4px;")
            try:
                btn.setIcon(qta.icon(icon_name, color="#888"))
                btn.setIconSize(QSize(16, 16))
            except Exception:
                btn.setText(tooltip[0])
            btn.clicked.connect(callback)
            quick_layout.addWidget(btn, alignment=Qt.AlignmentFlag.AlignHCenter)
            self._quick_btns.append((btn, icon_name))

        quick_layout.addStretch()
        h_layout.addWidget(self._quick_bar)

        # ── 右侧主内容
        right_widget = QWidget()
        right_widget.setMouseTracking(True)
        inner = QVBoxLayout(right_widget)
        inner.setContentsMargins(8, 10, 8, 8)
        inner.setSpacing(6)
        h_layout.addWidget(right_widget)

        # ── 标题栏（仅此处可拖动窗口，避免分组内框选时带动面板）
        self._title_bar = QWidget()
        self._title_bar.setMouseTracking(True)
        title_bar = QHBoxLayout(self._title_bar)
        title_bar.setContentsMargins(0, 0, 0, 0)
        title_bar.setSpacing(6)

        self.pin_btn = QPushButton()
        self.pin_btn.setFixedSize(26, 26)
        self.pin_btn.setToolTip(
            "固定：面板与悬浮球置顶；开启后最小化只收缩内容，不变悬浮球"
        )
        self.pin_btn.setStyleSheet("border:none; background:transparent;")
        self.pin_btn.clicked.connect(self._toggle_pin)

        # 程序名称
        self.app_title = QLabel("牛马软件柜")
        self.app_title.setStyleSheet(
            "font-size:13px; font-weight:bold; background:transparent;"
        )

        self._min_btn = QPushButton()
        self._min_btn.setFixedSize(26, 26)
        self._min_btn.setToolTip("最小化")
        self._min_btn.setStyleSheet("border:none; background:transparent;")
        self._min_btn.clicked.connect(self._on_min_click)

        title_bar.addWidget(self.pin_btn)
        title_bar.addWidget(self.app_title)
        title_bar.addStretch()
        title_bar.addWidget(self._min_btn)
        inner.addWidget(self._title_bar)

        # ── 顶部信息栏：左天气 + 右网络时间
        self.weather_bar = QWidget()
        self.weather_bar.setFixedHeight(80) # 强制整个天气栏高度对齐 WeatherBox
        wb_layout = QHBoxLayout(self.weather_bar)
        wb_layout.setContentsMargins(5, 0, 5, 0) # 上下边距设为 0，消除空隙
        wb_layout.setSpacing(10)

        self.weather_box = WeatherBox()
        wb_layout.addWidget(self.weather_box)

        self.weather_refresh_btn = QPushButton("刷新")
        self.weather_refresh_btn.setFixedSize(24, 24)
        self.weather_refresh_btn.setText("")
        self.weather_refresh_btn.setStyleSheet(
            "border:none; background:transparent; border-radius:6px;"
        )
        self.weather_refresh_btn.clicked.connect(self._refresh_weather)
        wb_layout.addWidget(self.weather_refresh_btn)

        wb_layout.addStretch()

        self.time_box = QWidget()
        tb_layout = QVBoxLayout(self.time_box)
        tb_layout.setContentsMargins(0, 0, 0, 0)
        tb_layout.setSpacing(0)

        self.time_title_label = QLabel("网络时间")
        self.time_title_label.setStyleSheet("font-size:11px; color:#888;")
        self.time_title_label.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
        )

        self.time_label = QLabel("--:--:--")
        self.time_label.setStyleSheet("font-weight:bold; font-size:18px; color:#ddd;")
        self.time_label.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
        )

        self.time_date_label = QLabel("--")
        self.time_date_label.setStyleSheet("font-size:11px; color:#888;")
        self.time_date_label.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
        )

        tb_layout.addWidget(self.time_title_label)
        tb_layout.addWidget(self.time_label)
        tb_layout.addWidget(self.time_date_label)

        wb_layout.addWidget(self.time_box)
        inner.addWidget(self.weather_bar)

        # ── 搜索框 + 文字按钮（同一行）
        search_row = QHBoxLayout()
        search_row.setContentsMargins(0, 0, 0, 0)
        search_row.setSpacing(6)

        self.search_box = QLineEdit()
        self.search_box.setPlaceholderText("🔍 搜索程序...")
        self.search_box.textChanged.connect(self._on_search)

        self.add_group_btn = QPushButton("添加分组")
        self.add_group_btn.setFixedHeight(32)
        self.add_group_btn.setToolTip("新建分组")
        self.add_group_btn.clicked.connect(self._add_group)

        self.add_folder_btn = QPushButton("添加文件夹")
        self.add_folder_btn.setFixedHeight(32)
        self.add_folder_btn.setToolTip("读取文件夹，自动创建分组")
        self.add_folder_btn.clicked.connect(self._add_from_folder)

        search_row.addWidget(self.search_box)
        search_row.addWidget(self.add_group_btn)
        search_row.addWidget(self.add_folder_btn)

        # ── 可收缩的内容区（搜索+分组+底部栏）
        self._body = QWidget()
        body_layout = QVBoxLayout(self._body)
        body_layout.setContentsMargins(0, 0, 0, 0)
        body_layout.setSpacing(6)
        body_layout.addLayout(search_row)

        # ── 分组滚动区
        self.scroll = QScrollArea()
        self.scroll.setWidgetResizable(True)
        self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

        self.groups_container = GroupsContainer()
        self.groups_container.setStyleSheet("background:transparent;")
        self.groups_layout = QVBoxLayout(self.groups_container)
        self.groups_layout.setContentsMargins(0, 0, 0, 0)
        self.groups_layout.setSpacing(6)
        self.groups_layout.addStretch()

        self.scroll.setWidget(self.groups_container)
        body_layout.addWidget(self.scroll)

        # ── 底部工具栏
        self.bottom_bar = QWidget()
        self.bottom_bar.setObjectName("bottom_bar")
        self.bottom_bar.setFixedHeight(36)
        bottom_layout = QHBoxLayout(self.bottom_bar)
        bottom_layout.setContentsMargins(6, 0, 6, 0)
        bottom_layout.setSpacing(4)

        self.menu_btn = QPushButton()
        self.menu_btn.setFixedSize(28, 28)
        self.menu_btn.setToolTip("菜单")
        self.menu_btn.setStyleSheet(
            "border:none; background:transparent; border-radius:4px;"
        )
        self.menu_btn.clicked.connect(self._show_bottom_menu)

        self.theme_btn = QPushButton()
        self.theme_btn.setFixedSize(28, 28)
        self.theme_btn.setToolTip("切换亮/暗主题")
        self.theme_btn.setStyleSheet(
            "border:none; background:transparent; border-radius:4px;"
        )
        self.theme_btn.clicked.connect(self._toggle_theme)

        self.settings_btn = QPushButton()
        self.settings_btn.setFixedSize(28, 28)
        self.settings_btn.setToolTip("设置")
        self.settings_btn.setStyleSheet(
            "border:none; background:transparent; border-radius:4px;"
        )
        self.settings_btn.clicked.connect(self._show_settings)

        self.quit_btn = QPushButton()
        self.quit_btn.setFixedSize(28, 28)
        self.quit_btn.setToolTip("退出程序")
        self.quit_btn.setStyleSheet(
            "border:none; background:transparent; border-radius:4px;"
        )
        self.quit_btn.clicked.connect(self._quit_application)

        bottom_layout.addWidget(self.menu_btn)
        bottom_layout.addStretch()
        self.update_btn = QPushButton()
        self.update_btn.setFixedSize(28, 28)
        self.update_btn.setToolTip("检查更新")
        self.update_btn.setStyleSheet("border:none; background:transparent; border-radius:4px;")
        self.update_btn.clicked.connect(lambda: self._updater.check(silent_if_latest=False))
        bottom_layout.addWidget(self.update_btn)
        bottom_layout.addWidget(self.theme_btn)
        bottom_layout.addWidget(self.settings_btn)
        bottom_layout.addWidget(self.quit_btn)
        body_layout.addWidget(self.bottom_bar)

        inner.addWidget(self._body)

        root.addWidget(self.container)
        # eventFilter 只用于 resize 拖拽，不拦截子 widget 的 drop
        self.container.installEventFilter(self)
        self.refresh_groups()

    def _apply_theme(self):
        t = theme.current()
        is_dark = theme.name() == "dark"
        ic = "#cccccc" if is_dark else "#555555"

        self.container.setStyleSheet(
            f"""
            QWidget#container {{
                background: {t['panel_bg']};
                border-radius: 10px;
                border: 1px solid {t['panel_border']};
            }}
        """
        )
        self.search_box.setStyleSheet(
            f"""
            QLineEdit {{
                background: {t['search_bg']};
                border: 1px solid {t['search_border']};
                border-radius: 5px;
                color: {t['search_color']};
                padding: 5px 10px;
                font-size: 12px;
            }}
            QLineEdit:focus {{ border-color: {t['search_focus']}; }}
        """
        )
        txt_btn_style = f"""
            QPushButton {{
                border: 1px solid {t['search_border']};
                border-radius: 5px;
                color: {t['btn_color']};
                background: {t['search_bg']};
                font-size: 11px;
                padding: 0 8px;
            }}
            QPushButton:hover {{ background: {t['header_hover']}; color: {t['btn_hover']}; }}
        """
        self.add_group_btn.setStyleSheet(txt_btn_style)
        self.add_folder_btn.setStyleSheet(txt_btn_style)
        self.scroll.setStyleSheet(
            f"""
            QScrollArea {{ border:none; background:transparent; }}
            QScrollBar:vertical {{ background:transparent; width:4px; }}
            QScrollBar::handle:vertical {{ background:{t['scrollbar']}; border-radius:2px; }}
            QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height:0; }}
        """
        )

        self.pin_btn.setIcon(
            qta.icon("fa5s.thumbtack", color="#f90" if self._pinned else ic)
        )
        self.pin_btn.setIconSize(QSize(13, 13))
        self._min_btn.setIcon(qta.icon("fa5s.minus", color=ic))
        self._min_btn.setIconSize(QSize(13, 13))
        self.app_title.setStyleSheet(
            f"font-size:13px; font-weight:bold; background:transparent; color:{t['search_color']};"
        )

        # 顶部天气栏
        self.weather_bar.setStyleSheet("background:transparent; border-radius:6px;")
        self.weather_box.set_theme_colors(t["search_color"])
        if hasattr(self, "weather_refresh_btn"):
            self.weather_refresh_btn.setStyleSheet(
                "border:none; background:transparent; border-radius:6px;"
            )
            # 刷新按钮图标跟随主题颜色
            try:
                self.weather_refresh_btn.setIcon(
                    qta.icon("fa5s.sync-alt", color=t["search_color"])
                )
                self.weather_refresh_btn.setIconSize(QSize(14, 14))
            except Exception:
                pass
        # 网络时间文字
        if hasattr(self, "time_label"):
            self.time_label.setStyleSheet(
                f"font-weight:bold; font-size:18px; color:{t['search_color']};"
            )
        if hasattr(self, "time_date_label"):
            self.time_date_label.setStyleSheet(
                f"font-size:11px; color:{t['search_color']};"
            )
        if hasattr(self, "time_title_label"):
            self.time_title_label.setStyleSheet("font-size:11px; color: #888;")

        # 左侧快捷栏
        bar_side_bg = "rgba(0,0,0,20)" if is_dark else "rgba(0,0,0,6)"
        self._quick_bar.setStyleSheet(
            f"""
            QWidget#quick_bar {{
                background: {bar_side_bg};
                border-right: 1px solid {t['panel_border']};
                border-radius: 10px 0 0 10px;
            }}
            QPushButton {{ border:none; background:transparent; border-radius:4px; }}
            QPushButton:hover {{ background:{t['header_hover']}; }}
        """
        )
        for btn, icon_name in self._quick_btns:
            try:
                btn.setIcon(qta.icon(icon_name, color=ic))
                btn.setIconSize(QSize(14, 14))
            except Exception:
                pass

        # 底部工具栏
        bar_bg2 = "rgba(0,0,0,30)" if is_dark else "rgba(0,0,0,8)"
        self.bottom_bar.setStyleSheet(
            f"""
            QWidget#bottom_bar {{
                background: {bar_bg2};
                border-radius: 0 0 10px 10px;
                border-top: 1px solid {t['panel_border']};
            }}
            QPushButton {{ border:none; background:transparent; border-radius:4px; }}
            QPushButton:hover {{ background:{t['header_hover']}; }}
        """
        )
        self.theme_btn.setIcon(
            qta.icon("fa5s.sun" if is_dark else "fa5s.moon", color=ic)
        )
        self.theme_btn.setIconSize(QSize(14, 14))
        self.update_btn.setIcon(qta.icon("fa5s.sync-alt", color=ic))
        self.update_btn.setIconSize(QSize(14, 14))
        self.menu_btn.setIcon(qta.icon("fa5s.bars", color=ic))
        self.menu_btn.setIconSize(QSize(14, 14))
        self.settings_btn.setIcon(qta.icon("fa5s.cog", color=ic))
        self.settings_btn.setIconSize(QSize(14, 14))
        self.quit_btn.setIcon(qta.icon("fa5s.sign-out-alt", color=ic))
        self.quit_btn.setIconSize(QSize(14, 14))

        for i in range(self.groups_layout.count() - 1):
            item = self.groups_layout.itemAt(i)
            if item and item.widget():
                item.widget()._apply_theme()

    def _quit_application(self):
        ret = dialog_style.question(
            self,
            "退出",
            "确认退出吗？",
            default_button=QMessageBox.StandardButton.No,
        )
        if ret == QMessageBox.StandardButton.Yes:
            QApplication.quit()

    def _restart_application(self):
        import subprocess
        subprocess.Popen([sys.executable] + sys.argv)
        QApplication.quit()

    def _open_admin_cmd(self):
        import ctypes
        ctypes.windll.shell32.ShellExecuteW(None, "runas", "cmd.exe", None, None, 1)

    def _open_admin_powershell(self):
        import ctypes
        ctypes.windll.shell32.ShellExecuteW(None, "runas", "powershell.exe", None, None, 1)

    def _open_default_browser(self):
        import webbrowser
        webbrowser.open("https://www.baidu.com")

    def _toggle_theme(self):
        theme.set_theme("light" if theme.name() == "dark" else "dark")
        self._apply_theme()
        # 若设置窗口已打开，同步更新其主题
        if hasattr(self, "_settings_win") and self._settings_win.isVisible():
            try:
                self._settings_win._apply_theme()
            except Exception:
                pass

    def _show_settings(self):
        from ui.settings_window import SettingsWindow

        if hasattr(self, "_settings_win") and self._settings_win.isVisible():
            self._settings_win.raise_()
            self._settings_win.activateWindow()
            return
        self._settings_win = SettingsWindow(self)
        self._settings_win.show()

    def _show_bottom_menu(self):
        menu = QMenu(self)
        menu.setStyleSheet(self._bottom_menu_stylesheet())
        menu.addAction(self._make_doubao_icon(), "豆包会话").triggered.connect(
            self._open_doubao_session
        )
        menu.addAction(
            self._make_round_menu_icon("D", "#4d6bfe"), "DeepSeek"
        ).triggered.connect(
            lambda: self._open_url_in_browser(
                "https://chat.deepseek.com/", "DeepSeek"
            )
        )
        menu.addAction(
            self._make_round_menu_icon("G", "#4285f4"), "Gemini"
        ).triggered.connect(
            lambda: self._open_url_in_browser(
                "https://gemini.google.com/", "Gemini"
            )
        )
        # 菜单贴近按钮下方显示，避免漂移到不自然位置
        pos = self.menu_btn.mapToGlobal(self.menu_btn.rect().bottomLeft())
        menu.exec(pos)

    def _bottom_menu_stylesheet(self) -> str:
        t = theme.current()
        return f"""
            QMenu {{
                background: {t['menu_bg']};
                color: {t['menu_color']};
                border: 1px solid {t['menu_border']};
                border-radius: 8px;
                padding: 4px;
            }}
            QMenu::item {{
                padding: 6px 14px;
                border-radius: 5px;
                background: transparent;
            }}
            QMenu::item:selected {{
                background: {t['menu_selected']};
            }}
            QMenu::separator {{
                height: 1px;
                background: {t['menu_border']};
                margin: 4px 8px;
            }}
        """

    def _make_doubao_icon(self) -> QIcon:
        return self._make_round_menu_icon("豆", "#3b82f6", font_pt=9)

    def _make_round_menu_icon(
        self, letter: str, bg_hex: str, *, font_pt: int = 9
    ) -> QIcon:
        pm = QPixmap(18, 18)
        pm.fill(Qt.GlobalColor.transparent)
        p = QPainter(pm)
        p.setRenderHint(QPainter.RenderHint.Antialiasing)
        p.setBrush(QBrush(QColor(bg_hex)))
        p.setPen(Qt.PenStyle.NoPen)
        p.drawEllipse(1, 1, 16, 16)
        p.setPen(QPen(QColor("white")))
        f = p.font()
        f.setPointSize(font_pt)
        f.setBold(True)
        p.setFont(f)
        p.drawText(pm.rect(), int(Qt.AlignmentFlag.AlignCenter), letter)
        p.end()
        return QIcon(pm)

    def _open_url_in_browser(self, url: str, title: str) -> None:
        try:
            ok = webbrowser.open(url)
        except Exception as e:
            dialog_style.warning(
                self, "打开失败", f"无法在默认浏览器中打开{title}：{e}"
            )
            return
        if not ok:
            dialog_style.warning(
                self,
                "打开失败",
                f"系统未注册可用的默认浏览器，请手动在浏览器中打开{title}：\n{url}",
            )

    def _open_doubao_session(self):
        """用系统默认浏览器打开豆包会话页。"""
        self._open_url_in_browser("https://www.doubao.com/chat/", "豆包")

    def refresh_groups(self):
        # 记录当前折叠状态
        collapsed_ids = set()
        for i in range(self.groups_layout.count() - 1):
            item = self.groups_layout.itemAt(i)
            if item and item.widget():
                gw = item.widget()
                if gw.collapsed:
                    collapsed_ids.add(gw.group_data["id"])

        while self.groups_layout.count() > 1:
            item = self.groups_layout.takeAt(0)
            if item.widget():
                item.widget().deleteLater()

        for gdata in database.get_groups():
            gw = GroupWidget(gdata)
            gw.request_refresh_all.connect(self.refresh_groups)
            # 恢复折叠状态
            if gdata["id"] in collapsed_ids:
                gw.collapsed = True
                gw.flow.setVisible(False)
                t = theme.current()
                ic = "#cccccc" if theme.name() == "dark" else "#555555"
                import qtawesome as _qta

                gw.toggle_icon.setPixmap(
                    _qta.icon("fa5s.chevron-right", color=ic).pixmap(14, 14)
                )
            self.groups_layout.insertWidget(self.groups_layout.count() - 1, gw)

    def reset_groups_and_items(self):
        """清空所有分组与程序，并刷新界面。"""
        database.reset_groups_and_items(recreate_default_group=True)
        # 清空搜索关键字，确保刷新后可见全部内容
        try:
            self.search_box.setText("")
        except Exception:
            pass
        self.refresh_groups()

    def _add_group(self):
        name, ok = dialog_style.get_text(self, "新建分组", "分组名称:")
        if ok and name.strip():
            database.add_group(name.strip())
            self.refresh_groups()

    def _add_from_folder(self):
        folder = dialog_style.get_existing_directory(self, "选择文件夹")
        if not folder:
            return
        # 统一路径分隔符
        folder = os.path.normpath(folder)
        group_name = os.path.basename(folder)
        gid = database.add_group(group_name, folder_path=folder)
        # 文件夹分组直接读目录，不写入 items 表
        count = sum(
            1 for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))
        )
        self.refresh_groups()
        dialog_style.information(
            self,
            "完成",
            f"已创建分组「{group_name}」，共 {count} 个文件（实时读取）。",
        )

    def _sync_pin_dependent_ui(self):
        """图钉图标、最小化提示、置顶标志（面板 + 悬浮球）。"""
        ic = "#cccccc" if theme.name() == "dark" else "#555555"
        self.pin_btn.setIcon(
            qta.icon("fa5s.thumbtack", color="#f90" if self._pinned else ic)
        )
        self.pin_btn.setIconSize(QSize(13, 13))
        if self._pinned:
            self._min_btn.setToolTip("收缩内容区（固定模式）")
        else:
            self._min_btn.setToolTip("最小化到悬浮球")
        self._apply_pin_window_layer()

    def _apply_pin_window_layer(self):
        """图钉开 → 面板与悬浮球 WindowStaysOnTop；关 → 普通叠放，不挡其他程序。"""
        on_top = self._pinned
        if getattr(self, "_pin_top_applied", None) is not on_top:
            self._pin_top_applied = on_top
            was_visible = self.isVisible()
            geo = self.geometry()
            flags = Qt.WindowType.FramelessWindowHint
            if on_top:
                flags |= Qt.WindowType.WindowStaysOnTopHint
            self.setWindowFlags(flags)
            self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
            self.setAcceptDrops(True)
            logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
            if os.path.exists(logo_path):
                self.setWindowIcon(QIcon(logo_path))
            if was_visible:
                self.setGeometry(geo)
                self.show()
        ball = getattr(self, "_ball_ref", None)
        if ball is not None:
            ball.set_stays_on_top(on_top)

    def _toggle_pin(self):
        self._pinned = not self._pinned
        self._sync_pin_dependent_ui()

    def _on_min_click(self):
        """图钉开启时收缩内容区，否则最小化到悬浮球"""
        if self._pinned:
            if self._body.isVisible():
                self._collapse_body()
            else:
                self._expand_body()
        else:
            self.minimize_to_ball()

    def _collapse_body(self):
        """收缩：隐藏内容区，窗口缩到只剩标题栏高度"""
        self._body.hide()
        self.weather_bar.hide()
        self._quick_bar.hide()
        self._expanded_height = self.height()
        title_h = self._title_bar.sizeHint().height() + 20
        self.setFixedHeight(title_h)
        ic = "#cccccc" if theme.name() == "dark" else "#555555"
        self._min_btn.setIcon(qta.icon("fa5s.chevron-down", color=ic))
        self._min_btn.setIconSize(QSize(13, 13))

    def _expand_body(self):
        """展开：恢复内容区和窗口高度"""
        self.weather_bar.show()
        self._body.show()
        self._quick_bar.show()
        h = getattr(self, "_expanded_height", 520)
        self.setMinimumHeight(MIN_H)
        self.setMaximumHeight(16777215)
        self.resize(self.width(), h)
        self._persist_w = self.width()
        self._persist_h = h
        ic = "#cccccc" if theme.name() == "dark" else "#555555"
        self._min_btn.setIcon(qta.icon("fa5s.minus", color=ic))
        self._min_btn.setIconSize(QSize(13, 13))

    def _save_geometry(self):
        """把当前位置和尺寸写入数据库"""
        g = self.geometry()
        database.set_setting("panel_x", str(g.x()))
        database.set_setting("panel_y", str(g.y()))
        # 收缩为仅标题栏时高度变小，勿把临时高度写入库/覆盖跨屏用的 persist
        if getattr(self, "_body", None) is not None and not self._body.isVisible():
            return
        self._persist_w = g.width()
        self._persist_h = g.height()
        database.set_setting("panel_w", str(g.width()))
        database.set_setting("panel_h", str(g.height()))

    def _restore_geometry(self):
        """从数据库恢复上次的位置、尺寸和透明度"""
        try:
            x = int(database.get_setting("panel_x", ""))
            y = int(database.get_setting("panel_y", ""))
            w = int(database.get_setting("panel_w", str(PANEL_W)))
            h = int(database.get_setting("panel_h", str(PANEL_H)))
            # 用保存坐标所在屏幕来限制范围，支持多显示器
            screen_obj = QApplication.screenAt(QPoint(x, y))
            if screen_obj is None:
                screen_obj = QApplication.primaryScreen()
            screen = screen_obj.availableGeometry()
            x = max(screen.left(), min(x, screen.right() - w))
            y = max(screen.top(), min(y, screen.bottom() - h))
            self._persist_w = w
            self._persist_h = h
            self.setGeometry(x, y, w, h)
        except (ValueError, TypeError):
            pass
        # 恢复透明度
        try:
            opacity = int(database.get_setting("panel_opacity", "100"))
            self.setWindowOpacity(max(30, min(100, opacity)) / 100)
        except (ValueError, TypeError):
            pass

    def _on_search(self, keyword):
        for i in range(self.groups_layout.count() - 1):
            item = self.groups_layout.itemAt(i)
            if item and item.widget():
                item.widget().filter(keyword)

    # ── 显示/隐藏 ────────────────────────────────────────
    def show_near(self, ball_pos: QPoint, ball_size: int):
        # 根据球所在屏幕来定位 panel，避免跑到主屏幕
        screen_obj = QApplication.screenAt(ball_pos)
        if screen_obj is None:
            screen_obj = QApplication.primaryScreen()
        screen = screen_obj.availableGeometry()
        pw, ph = self.width(), self.height()
        bx, by = ball_pos.x(), ball_pos.y()

        x = bx - pw - 8
        if x < screen.left():
            x = bx + ball_size + 8
        y = max(screen.top(), min(by + ball_size // 2 - ph // 2, screen.bottom() - ph))

        self.move(x, y + 30)
        self.setWindowOpacity(0)
        self.show()
        self.raise_()

        if hasattr(self, "_ball_ref"):
            self._ball_ref.hide()

        if self._anim:
            self._anim.stop()
        self._anim = QPropertyAnimation(self, b"windowOpacity")
        self._anim.setDuration(ANIM_MS)
        self._anim.setStartValue(0.0)
        self._anim.setEndValue(1.0)
        self._anim.start()

        self._anim2 = QPropertyAnimation(self, b"pos")
        self._anim2.setDuration(ANIM_MS)
        self._anim2.setEasingCurve(QEasingCurve.Type.OutCubic)
        self._anim2.setStartValue(QPoint(x, y + 30))
        self._anim2.setEndValue(QPoint(x, y))
        self._anim2.finished.connect(self._save_geometry)
        self._anim2.start()

    def hide_panel(self):
        if self._anim:
            self._anim.stop()
        self._anim = QPropertyAnimation(self, b"windowOpacity")
        self._anim.setDuration(ANIM_MS)
        self._anim.setStartValue(1.0)
        self._anim.setEndValue(0.0)
        self._anim.finished.connect(self.hide)
        self._anim.start()

    def minimize_to_ball(self):
        self.hide_panel()
        if hasattr(self, "_ball_ref"):
            self._ball_ref.place_on_screen_of(self)
            self._ball_ref.show()
            self._ball_ref.setWindowOpacity(1.0)

    def toggle_near(self, ball_pos: QPoint, ball_size: int):
        if self.isVisible():
            if not self._pinned:
                self.minimize_to_ball()
        else:
            self.show_near(ball_pos, ball_size)

    # ── 边缘检测 ─────────────────────────────────────────
    def _edge_at(self, p: QPoint):
        x, y, w, h = p.x(), p.y(), self.width(), self.height()
        m = RESIZE_M
        l = x <= m
        r = x >= w - m
        t = y <= m
        b = y >= h - m
        if b and r:
            return "br"
        if b and l:
            return "bl"
        if t and r:
            return "tr"
        if t and l:
            return "tl"
        if r:
            return "r"
        if l:
            return "l"
        if b:
            return "b"
        if t:
            return "t"
        return None

    _EDGE_CUR = {
        "r": Qt.CursorShape.SizeHorCursor,
        "l": Qt.CursorShape.SizeHorCursor,
        "b": Qt.CursorShape.SizeVerCursor,
        "t": Qt.CursorShape.SizeVerCursor,
        "br": Qt.CursorShape.SizeFDiagCursor,
        "tl": Qt.CursorShape.SizeFDiagCursor,
        "bl": Qt.CursorShape.SizeBDiagCursor,
        "tr": Qt.CursorShape.SizeBDiagCursor,
    }

    def _update_cursor(self, global_pos: QPoint):
        """根据全局坐标更新光标形状（不依赖事件来源 widget）"""
        local = self.mapFromGlobal(global_pos)
        edge = self._edge_at(local)
        self.setCursor(self._EDGE_CUR.get(edge, Qt.CursorShape.ArrowCursor))

    def _do_resize(self, gp: QPoint):
        d = gp - self._resize_start_global
        g = self._resize_start_geo
        x, y, w, h = g.x(), g.y(), g.width(), g.height()
        e = self._resize_edge
        if "r" in e:
            w = max(MIN_W, w + d.x())
        if "l" in e:
            nw = max(MIN_W, w - d.x())
            x += w - nw
            w = nw
        if "b" in e:
            h = max(MIN_H, h + d.y())
        if "t" in e:
            nh = max(MIN_H, h - d.y())
            y += h - nh
            h = nh
        self.setGeometry(x, y, w, h)

    def _container_pos_is_title_bar(self, pos_in_container: QPoint) -> bool:
        """仅标题栏区域允许拖动整个面板。"""
        w = self.container.childAt(pos_in_container)
        p = w
        while p:
            if p is self._title_bar:
                return True
            p = p.parentWidget()
        return False

    # ── eventFilter：处理 container 上的 resize/drag ─────
    def eventFilter(self, obj, event):
        if obj is self.container:
            et = event.type()
            # drop 事件放行给子 widget
            if et in (
                QEvent.Type.DragEnter,
                QEvent.Type.DragMove,
                QEvent.Type.Drop,
                QEvent.Type.DragLeave,
            ):
                return False

            if (
                et == QEvent.Type.MouseButtonPress
                and event.button() == Qt.MouseButton.LeftButton
            ):
                local = self.mapFromGlobal(event.globalPosition().toPoint())
                edge = self._edge_at(local)
                if edge:
                    self._resizing = True
                    self._resize_edge = edge
                    self._resize_start_global = event.globalPosition().toPoint()
                    self._resize_start_geo = self.geometry()
                    return True
                pos_c = self.container.mapFrom(self, local)
                if not self._container_pos_is_title_bar(pos_c):
                    return False
                self._win_drag_pos = (
                    event.globalPosition().toPoint() - self.frameGeometry().topLeft()
                )

            elif et == QEvent.Type.MouseMove:
                gp = event.globalPosition().toPoint()
                if self._resizing:
                    self._do_resize(gp)
                    return True
                if event.buttons() & Qt.MouseButton.LeftButton and self._win_drag_pos:
                    self.move(gp - self._win_drag_pos)
                    return True
                # 悬停时更新光标
                self._update_cursor(gp)

            elif et == QEvent.Type.MouseButtonRelease:
                self._resizing = False
                self._resize_edge = None
                self._win_drag_pos = None
                self._update_cursor(event.globalPosition().toPoint())
                self._save_geometry()

        return super().eventFilter(obj, event)

    # ── PanelWindow 自身的鼠标事件（鼠标在 container 外时）
    def mouseMoveEvent(self, event):
        if self._resizing:
            self._do_resize(event.globalPosition().toPoint())
            return
        if event.buttons() & Qt.MouseButton.LeftButton and self._win_drag_pos:
            self.move(event.globalPosition().toPoint() - self._win_drag_pos)
            return
        self._update_cursor(event.globalPosition().toPoint())

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            edge = self._edge_at(event.pos())
            if edge:
                self._resizing = True
                self._resize_edge = edge
                self._resize_start_global = event.globalPosition().toPoint()
                self._resize_start_geo = self.geometry()
            elif self.container.geometry().contains(event.pos()):
                pos_c = self.container.mapFrom(self, event.pos())
                if self._container_pos_is_title_bar(pos_c):
                    self._win_drag_pos = (
                        event.globalPosition().toPoint()
                        - self.frameGeometry().topLeft()
                    )

    def mouseReleaseEvent(self, event):
        self._resizing = False
        self._resize_edge = None
        self._win_drag_pos = None
        self._update_cursor(event.globalPosition().toPoint())
        self._save_geometry()  # 拖动/resize 结束后保存位置和尺寸

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()

    def focusOutEvent(self, event):
        if not self._pinned:
            QTimer.singleShot(200, self._check_focus_lost)

    def _check_focus_lost(self):
        if not self.isActiveWindow() and not self._pinned:
            self.hide_panel()

    # ── 托盘 ─────────────────────────────────────────────
    def _setup_tray(self):
        self.tray = QSystemTrayIcon(self)
        self.tray.setIcon(_make_tray_icon())
        self.tray.setToolTip("桌面文件整理")

        menu = QMenu()
        menu.setStyleSheet(
            """
            QMenu { background:#2b2b2b; color:#eee; border:1px solid #555;
                    padding:4px; border-radius:6px; }
            QMenu::item { padding:6px 20px; border-radius:4px; }
            QMenu::item:selected { background:#3a3a3a; }
            QMenu::separator { height:1px; background:#444; margin:4px 8px; }
        """
        )
        for tooltip, _icon, callback in self._get_quick_actions():
            menu.addAction(tooltip).triggered.connect(callback)
        menu.addSeparator()
        menu.addAction("🔄  重启本程序").triggered.connect(self._restart_application)
        menu.addAction("❌  退出本程序").triggered.connect(QApplication.quit)

        self.tray.setContextMenu(menu)
        self.tray.activated.connect(self._on_tray_activated)
        self.tray.show()

    def _on_tray_activated(self, reason):
        if reason in (
            QSystemTrayIcon.ActivationReason.Trigger,
            QSystemTrayIcon.ActivationReason.DoubleClick,
        ):
            if self.isVisible():
                self.minimize_to_ball()
            elif hasattr(self, "_ball_ref"):
                self.show_near(self._ball_ref.pos(), self._ball_ref.width())

    def _toggle_autostart(self):
        self._autostart_enabled = not self._autostart_enabled
        _set_autostart(self._autostart_enabled)
        self._autostart_act.setText(
            f"🚀  开机自启: {'开' if self._autostart_enabled else '关'}"
        )


class SettingsPopup(QWidget):
    """设置弹出面板：透明度调节等"""

    def __init__(self, panel: "PanelWindow"):
        super().__init__(panel, Qt.WindowType.Popup)
        self._panel = panel
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        self.setWindowFlags(Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint)
        self._build()

    def _build(self):
        from PyQt6.QtWidgets import QSlider, QLabel, QVBoxLayout, QHBoxLayout

        t = theme.current()
        is_dark = theme.name() == "dark"

        self.setStyleSheet(
            f"""
            QWidget {{
                background: {t['panel_bg']};
                border: 1px solid {t['panel_border']};
                border-radius: 8px;
                color: {t['search_color']};
            }}
            QSlider::groove:horizontal {{
                height: 4px;
                background: {t['scrollbar']};
                border-radius: 2px;
            }}
            QSlider::handle:horizontal {{
                width: 14px; height: 14px;
                margin: -5px 0;
                background: #4a9eff;
                border-radius: 7px;
            }}
            QSlider::sub-page:horizontal {{
                background: #4a9eff;
                border-radius: 2px;
            }}
        """
        )

        layout = QVBoxLayout(self)
        layout.setContentsMargins(14, 12, 14, 12)
        layout.setSpacing(10)

        # 透明度
        row = QHBoxLayout()
        row.setSpacing(10)
        lbl = QLabel("透明度")
        lbl.setStyleSheet(
            f"color:{t['search_color']}; font-size:12px; background:transparent; border:none;"
        )
        lbl.setFixedWidth(42)

        self._opacity_slider = QSlider(Qt.Orientation.Horizontal)
        self._opacity_slider.setRange(30, 100)
        saved_opacity = int(database.get_setting("panel_opacity", "100"))
        self._opacity_slider.setValue(saved_opacity)
        self._opacity_slider.setFixedWidth(140)
        self._opacity_slider.valueChanged.connect(self._on_opacity)

        self._opacity_val = QLabel(f"{self._opacity_slider.value()}%")
        self._opacity_val.setStyleSheet(
            f"color:{t['search_color']}; font-size:11px; background:transparent; border:none;"
        )
        self._opacity_val.setFixedWidth(34)

        row.addWidget(lbl)
        row.addWidget(self._opacity_slider)
        row.addWidget(self._opacity_val)
        layout.addLayout(row)

        self.adjustSize()

    def _on_opacity(self, val: int):
        self._panel.setWindowOpacity(val / 100)
        self._opacity_val.setText(f"{val}%")
        database.set_setting("panel_opacity", str(val))


def _set_autostart(enable: bool):
    try:
        import winreg

        key = winreg.OpenKey(
            winreg.HKEY_CURRENT_USER,
            r"Software\Microsoft\Windows\CurrentVersion\Run",
            0,
            winreg.KEY_SET_VALUE,
        )
        app_name = "DesktopOrganizer"
        if enable:
            exe = (
                f'"{sys.executable}" "{os.path.abspath("main.py")}"'
                if not getattr(sys, "frozen", False)
                else f'"{sys.executable}"'
            )
            winreg.SetValueEx(key, app_name, 0, winreg.REG_SZ, exe)
        else:
            try:
                winreg.DeleteValue(key, app_name)
            except FileNotFoundError:
                pass
        winreg.CloseKey(key)
    except Exception as e:
        print(f"自启设置失败: {e}")
