# -*- coding: utf-8 -*-
"""
运行目录日志：顶级「日志查看」下含「日志搜索」「全局搜索」子页签；单文件阅读/trace 与 rg 目录搜索。
"""

from __future__ import annotations

import os
import re
import subprocess
from pathlib import Path
from typing import Any, Optional

from PyQt6.QtCore import Qt, QDate, QTimer
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QComboBox,
    QDateEdit,
    QFrame,
    QGridLayout,
    QHBoxLayout,
    QInputDialog,
    QLabel,
    QLineEdit,
    QMessageBox,
    QPlainTextEdit,
    QPushButton,
    QRadioButton,
    QTabWidget,
    QVBoxLayout,
    QWidget,
)

from views.logs.log_viewer_pane import LogViewerStandaloneDialog

from services.rg_search import find_rg_executable, rg_search_directory

from services.commands import (
    flatten_server_dirs,
    get_local_ip,
    get_server_list,
    read_text_file_best_effort,
    resolve_log_file_path,
    rpc_role_gs_trace_network,
)


def _run_dir_from_config(config: Any) -> str:
    server_root = config.get("workspace", "server_root", default="")
    run_dir = config.get("workspace", "run_dir", default="")
    if not run_dir and server_root:
        run_dir = str(Path(server_root) / "run")
    return run_dir or ""


def _refresh_server_combo(combo: QComboBox, config: Any) -> bool:
    combo.clear()
    run_dir = _run_dir_from_config(config)
    if not run_dir or not Path(run_dir).exists():
        combo.addItem("(请先配置运行目录)")
        return False
    for name in flatten_server_dirs(run_dir):
        combo.addItem(name)
    return True


class _LogStyleMixin:
    """日志页签共用样式与标签。"""

    def _pill_label(self, text: str) -> QLabel:
        lb = QLabel(text)
        lb.setStyleSheet(
            """
            QLabel {
                color: #e8eef7;
                background-color: transparent;
                padding: 2px 4px;
                font-size: 12px;
                min-width: 86px;
            }
            """
        )
        return lb

    def _combo_style(self) -> str:
        return (
            "QComboBox { background-color: #242932; color: #e8eef7; border: 1px solid #3a4352; "
            "border-radius: 6px; padding: 6px 10px; min-height: 26px; }"
            "QComboBox::drop-down { border: none; }"
            "QComboBox QAbstractItemView {"
            " background-color: #242932; color: #e8eef7; border: 1px solid #3a4352;"
            " selection-background-color: #4f8cff; selection-color: #e8eef7; outline: none; }"
            "QComboBox QAbstractItemView::item { padding: 6px 8px; border: none; min-height: 24px; }"
            "QComboBox QAbstractItemView::item:selected { background-color: #4f8cff; color: #e8eef7; border: none; }"
            "QComboBox QAbstractItemView::item:hover { background-color: #243247; color: #e8eef7; border: none; }"
        )

    def _input_style(self) -> str:
        return (
            "QLineEdit, QDateEdit { background-color: #242932; color: #e8eef7; "
            "border: 1px solid #3a4352; border-radius: 6px; padding: 6px 10px; min-height: 26px; }"
        )

    def _btn_style(self, bg: str = "#2b313c", hover: str = "#263a55", fg: str = "#c6d7ef", border: str = "#4f6b92") -> str:
        return (
            f"QPushButton {{ background-color: {bg}; color: {fg}; border: 1px solid {border}; "
            f"border-radius: 6px; padding: 7px 16px; font-weight: 600; min-height: 28px; }}"
            f"QPushButton:hover {{ background-color: {hover}; }}"
        )

    def _primary_btn_style(self) -> str:
        return self._btn_style("#4f8cff", "#3f7df0", "#e8eef7", "#4f8cff")

    def _danger_btn_style(self) -> str:
        return self._btn_style("#342229", "#3e2730", "#ff5c6a", "#8b3941")

    def _neutral_btn_style(self) -> str:
        return self._btn_style("#242932", "#2b313c", "#aab4c3", "#3a4352")


class LogLoadTab(_LogStyleMixin, QWidget):
    """加载单文件日志、Trace；成功后打开独立阅读窗口。"""

    _PRESET_LOGS = (
        "error.log",
        "debug.log",
        "http_info.log",
        "http_error.log",
        "info.log",
        "http.log",
    )

    def __init__(self, config: Any, console: Any):
        super().__init__()
        self.config = config
        self.console = console
        self._standalone_window: Optional[LogViewerStandaloneDialog] = None
        self._build_ui()

    def _build_ui(self):
        root = QVBoxLayout(self)
        root.setSpacing(10)
        root.setContentsMargins(10, 10, 10, 8)

        toolbar = QFrame()
        toolbar.setObjectName("logToolbar")
        toolbar.setStyleSheet(
            """
            QFrame#logToolbar {
                background-color: #242932;
                border: 1px solid #3a4352;
                border-radius: 8px;
            }
            """
        )
        tb_layout = QVBoxLayout(toolbar)
        tb_layout.setSpacing(9)
        tb_layout.setContentsMargins(16, 12, 16, 12)

        title_row = QHBoxLayout()
        title = QLabel("日志搜索")
        title.setStyleSheet("color: #e8eef7; font-size: 14px; font-weight: 700;")
        title_row.addWidget(title)
        title_row.addStretch()
        tb_layout.addLayout(title_row)

        row1 = QHBoxLayout()
        row1.setSpacing(10)
        row1.addWidget(self._pill_label("服务器目录"))
        self.server_combo = QComboBox()
        self.server_combo.setStyleSheet(self._combo_style())
        self.server_combo.setMinimumWidth(360)
        row1.addWidget(self.server_combo, 1)
        tb_layout.addLayout(row1)

        type_row = QHBoxLayout()
        type_row.setSpacing(16)
        type_row.addWidget(self._pill_label("日志类型"))
        self.radio_server = QRadioButton("服务器日志")
        self.radio_server.setChecked(True)
        self.radio_codec = QRadioButton("玩家协议 (tag_log)")
        self.radio_server.toggled.connect(self._on_mode_changed)
        self.radio_codec.toggled.connect(self._on_mode_changed)
        type_row.addWidget(self.radio_server)
        type_row.addWidget(self.radio_codec)
        type_row.addStretch()
        tb_layout.addLayout(type_row)

        file_row = QHBoxLayout()
        file_row.setSpacing(10)

        self.preset_combo = QComboBox()
        for p in self._PRESET_LOGS:
            self.preset_combo.addItem(p)
        self.preset_combo.setEditable(False)
        self.preset_combo.setStyleSheet(self._combo_style())
        self.preset_combo.setMinimumWidth(220)
        self.custom_name = QLineEdit()
        self.custom_name.setPlaceholderText("例如 crash.log（留空则用上方选择）")
        self.custom_name.setStyleSheet(self._input_style())
        self.custom_name.setMinimumWidth(260)

        self.role_input = QLineEdit()
        self.role_input.setPlaceholderText("例如 10000100")
        self.role_input.setStyleSheet(self._input_style())
        self.role_input.setMinimumWidth(260)

        self.preset_label = self._pill_label("预设")
        file_row.addWidget(self.preset_label)
        file_row.addWidget(self.preset_combo)
        file_row.addWidget(self.custom_name, 1)
        tb_layout.addLayout(file_row)

        role_row = QHBoxLayout()
        role_row.setSpacing(10)
        self.role_row_label = self._pill_label("角色 ID")
        role_row.addWidget(self.role_row_label)
        role_row.addWidget(self.role_input, 1)
        tb_layout.addLayout(role_row)

        date_row = QHBoxLayout()
        date_row.setSpacing(10)
        self.use_date_cb = QCheckBox("指定归档日期")
        self.use_date_cb.toggled.connect(self._toggle_date)
        self.date_edit = QDateEdit()
        self.date_edit.setCalendarPopup(True)
        self.date_edit.setDisplayFormat("yyyy-MM-dd")
        self.date_edit.setDate(QDate.currentDate())
        self.date_edit.setMaximumDate(QDate.currentDate())
        self.date_edit.setEnabled(False)
        self.date_edit.setStyleSheet(self._input_style())
        self.date_edit.setFixedWidth(140)
        date_row.addWidget(self._pill_label("归档日期"))
        date_row.addWidget(self.use_date_cb)
        date_row.addWidget(self.date_edit)
        date_row.addStretch()
        tb_layout.addLayout(date_row)

        load_top = QHBoxLayout()
        load_top.setSpacing(10)
        self.load_btn = QPushButton("加载日志")
        self.load_btn.setCursor(Qt.CursorShape.PointingHandCursor)
        self.load_btn.setStyleSheet(self._primary_btn_style())
        self.load_btn.setMinimumHeight(36)
        self.load_btn.setMinimumWidth(116)
        self.load_btn.clicked.connect(self.load_log)
        load_top.addWidget(self.load_btn)

        self.trace_on_btn = QPushButton("开启玩家协议")
        self.trace_off_btn = QPushButton("关闭玩家协议")
        for b in (self.trace_on_btn, self.trace_off_btn):
            b.setCursor(Qt.CursorShape.PointingHandCursor)
            b.setStyleSheet(self._btn_style())
            b.setMinimumHeight(36)
            b.setMinimumWidth(130)
        self.trace_on_btn.clicked.connect(lambda: self._prompt_trace(True))
        self.trace_off_btn.clicked.connect(lambda: self._prompt_trace(False))
        load_top.addWidget(self.trace_on_btn)
        load_top.addWidget(self.trace_off_btn)
        self.open_log_dir_btn = QPushButton("打开文件夹")
        self.open_log_dir_btn.setCursor(Qt.CursorShape.PointingHandCursor)
        self.open_log_dir_btn.setStyleSheet(self._neutral_btn_style())
        self.open_log_dir_btn.setMinimumHeight(36)
        self.open_log_dir_btn.setMinimumWidth(120)
        self.open_log_dir_btn.setToolTip(
            "在资源管理器中打开当前选中服务器的日志目录（run/…/log，协议模式为 log/tag_log）"
        )
        self.open_log_dir_btn.clicked.connect(self.open_log_directory)
        load_top.addWidget(self.open_log_dir_btn)
        load_top.addStretch()
        tb_layout.addLayout(load_top)

        self.path_label = QLabel("")

        root.addWidget(toolbar)

        content_frame = QFrame()
        content_frame.setObjectName("logContentFrame")
        content_frame.setStyleSheet(
            """
            QFrame#logContentFrame {
                background-color: #242932;
                border: 1px solid #3a4352;
                border-radius: 8px;
            }
            """
        )
        content_layout = QVBoxLayout(content_frame)
        content_layout.setContentsMargins(14, 10, 14, 12)
        content_layout.setSpacing(8)
        content_header = QHBoxLayout()
        content_title = QLabel("日志内容")
        content_title.setStyleSheet("color: #e8eef7; font-size: 14px; font-weight: 700;")
        content_header.addWidget(content_title)
        content_header.addStretch()
        self.auto_scroll_cb = QCheckBox("自动滚动")
        self.auto_scroll_cb.setChecked(True)
        content_header.addWidget(self.auto_scroll_cb)
        self.standalone_btn = QPushButton("独立窗口")
        self.standalone_btn.setStyleSheet(self._btn_style())
        self.standalone_btn.setMinimumWidth(104)
        self.standalone_btn.clicked.connect(self._open_current_in_standalone)
        content_header.addWidget(self.standalone_btn)
        self.clear_content_btn = QPushButton("清空")
        self.clear_content_btn.setStyleSheet(self._danger_btn_style())
        self.clear_content_btn.setMinimumWidth(78)
        self.clear_content_btn.clicked.connect(self._clear_embedded_log)
        content_header.addWidget(self.clear_content_btn)
        content_layout.addLayout(content_header)

        self.log_content = QPlainTextEdit()
        self.log_content.setReadOnly(True)
        self.log_content.setFont(QFont("Consolas", 10))
        self.log_content.setStyleSheet(
            """
            QPlainTextEdit {
                background-color: #242932;
                color: #e7edf6;
                border: 1px solid #3a4352;
                border-radius: 8px;
                padding: 9px 10px;
                selection-background-color: #2b4d75;
            }
            """
        )
        content_layout.addWidget(self.log_content, 1)
        root.addWidget(content_frame, 1)

        self._current_log_title = ""
        self._current_log_content = ""

        self.load_hint = QLabel("")
        self.load_hint.setWordWrap(True)
        self.load_hint.setStyleSheet("color: #aab4c3; font-size: 11px;")
        root.addWidget(self.load_hint)
        self._on_mode_changed()

    def refresh_server_list(self):
        if not _refresh_server_combo(self.server_combo, self.config):
            self.load_hint.setStyleSheet("color: #ff5c6a; font-size: 11px;")
            self.load_hint.setText("未配置或无效，请在「工具设置」中设置运行目录。")
        elif not self.load_hint.text() or "运行目录" in self.load_hint.text():
            self.load_hint.setStyleSheet("color: #aab4c3; font-size: 11px;")
            self.load_hint.setText("")

    def _on_mode_changed(self):
        server_mode = self.radio_server.isChecked()
        self.preset_label.setVisible(server_mode)
        self.preset_combo.setVisible(server_mode)
        self.custom_name.setVisible(server_mode)
        self.role_row_label.setVisible(not server_mode)
        self.role_input.setVisible(not server_mode)

    def _toggle_date(self, on: bool):
        self.date_edit.setEnabled(on)

    def _base_filename(self) -> Optional[str]:
        if self.radio_server.isChecked():
            text = (self.custom_name.text() or "").strip()
            if text:
                return text
            return (self.preset_combo.currentText() or "").strip() or None
        role = (self.role_input.text() or "").strip()
        if not role:
            return None
        if not re.fullmatch(r"\d+", role):
            return None
        return f"network_codec-{role}.log"

    def _date_yyyymmdd(self) -> Optional[str]:
        if not self.use_date_cb.isChecked():
            return None
        return self.date_edit.date().toString("yyyyMMdd")

    def _subdir(self) -> str:
        return "" if self.radio_server.isChecked() else "tag_log"

    def _open_standalone(self, title: str, content: str):
        if self._standalone_window is None:
            self._standalone_window = LogViewerStandaloneDialog(self)
        self._standalone_window.set_log(title, content)
        self._standalone_window.show()
        self._standalone_window.raise_()
        self._standalone_window.activateWindow()

    def load_log(self):
        self.load_hint.setText("")
        self.load_hint.setStyleSheet("color: #aab4c3; font-size: 11px;")
        run_dir = _run_dir_from_config(self.config)
        if not run_dir or not Path(run_dir).exists():
            self._set_load_error("请先于「工具设置」配置有效的运行目录。")
            return
        idx = self.server_combo.currentIndex()
        if idx < 0:
            self._set_load_error("请选择服务器目录。")
            return
        server_dir = self.server_combo.currentText()
        if server_dir.startswith("("):
            self._set_load_error("无可用服务器目录。")
            return
        base = self._base_filename()
        if not base:
            if self.radio_codec.isChecked():
                self._set_load_error("请填写有效的角色 ID（纯数字）。")
            else:
                self._set_load_error("请选择或填写日志文件名。")
            return
        path = resolve_log_file_path(
            run_dir, server_dir, base, self._date_yyyymmdd(), self._subdir()
        )
        path_str = str(path)
        self.path_label.setText(path_str)
        content, err = read_text_file_best_effort(path)
        if err:
            self._set_load_error(f"{err}\n路径: {path_str}")
            if self.console:
                self.console.append_output(f"[日志] {err}\n")
            return
        self.load_hint.setStyleSheet("color: #25c889; font-size: 11px;")
        self.load_hint.setText("日志已加载。")
        self._current_log_title = f"日志 — {path_str}"
        self._current_log_content = content
        self.log_content.setPlainText(content)
        if self.auto_scroll_cb.isChecked():
            self.log_content.moveCursor(self.log_content.textCursor().MoveOperation.End)
        if self.console:
            self.console.append_output(f"[日志] 已加载 {path_str} ({len(content)} 字符)\n")

    def _set_load_error(self, msg: str):
        self.load_hint.setStyleSheet("color: #ff5c6a; font-size: 11px;")
        self.load_hint.setText(msg)

    def _open_current_in_standalone(self):
        content = self._current_log_content or self.log_content.toPlainText()
        if not content:
            QMessageBox.information(self, "独立窗口", "请先加载日志。")
            return
        self._open_standalone(self._current_log_title or "日志", content)

    def _clear_embedded_log(self):
        self._current_log_content = ""
        self._current_log_title = ""
        self.log_content.clear()
        self.load_hint.setText("")

    def _project_root(self) -> Path:
        return Path(self.config.get("workspace", "server_root", default="") or Path.cwd())

    def _open_project_directory(self):
        root = self._project_root()
        if not root.exists():
            QMessageBox.warning(self, "打开目录", f"目录不存在：\n{root}")
            return
        try:
            if os.name == "nt":
                os.startfile(str(root.resolve()))
            else:
                subprocess.Popen(["xdg-open", str(root.resolve())])
        except Exception as e:
            QMessageBox.warning(self, "打开目录", f"打开失败：{e}")

    def _copy_project_path(self):
        QApplication.clipboard().setText(str(self._project_root()))
        self.load_hint.setStyleSheet("color: #25c889; font-size: 11px;")
        self.load_hint.setText("项目路径已复制。")

    def open_log_directory(self):
        """打开当前选中服务器下的日志目录（run/.../log 或 log/tag_log），不依赖是否已加载文件。"""
        run_dir = _run_dir_from_config(self.config)
        if not run_dir or not Path(run_dir).exists():
            QMessageBox.warning(self, "打开文件夹", "请先于「工具设置」配置有效的运行目录。")
            return
        server_dir = self.server_combo.currentText()
        if server_dir.startswith("("):
            QMessageBox.information(self, "打开文件夹", "请选择服务器目录。")
            return
        base_log = Path(run_dir) / server_dir / "log"
        sub = self._subdir()
        folder = (base_log / sub) if sub else base_log
        if sub and not folder.is_dir() and base_log.is_dir():
            folder = base_log
        if not folder.is_dir():
            QMessageBox.information(
                self,
                "打开文件夹",
                f"目录不存在：\n{folder}\n\n请先在该服务器下产生日志或检查路径。",
            )
            return
        try:
            if os.name == "nt":
                os.startfile(str(folder.resolve()))
            else:
                subprocess.Popen(["xdg-open", str(folder.resolve())])
        except Exception as e:
            QMessageBox.warning(self, "打开文件夹", f"打开失败：{e}")

    def _prompt_trace(self, enable: bool):
        run_dir = _run_dir_from_config(self.config)
        if not run_dir or not Path(run_dir).exists():
            self._toast_console("运行目录无效，无法 Trace。")
            return
        server_dir = self.server_combo.currentText()
        if server_dir.startswith("("):
            self._toast_console("请选择有效的服务器目录。")
            return
        game_dirs = get_server_list(run_dir).get("game") or []
        if server_dir not in game_dirs:
            self._toast_console(
                "Trace 仅支持当前运行目录下的「游戏服」节点（目录名需匹配 game 类型，如 *_game_s*）。"
            )
            return
        dlg_title = "开启玩家协议" if enable else "关闭玩家协议"
        default_rid = (self.role_input.text() or "").strip()
        text, ok = QInputDialog.getText(
            self,
            dlg_title,
            "请输入角色 ID（纯数字）：",
            QLineEdit.EchoMode.Normal,
            default_rid,
        )
        if not ok:
            return
        tip = (text or "").strip()
        if not tip or not re.fullmatch(r"\d+", tip):
            self._toast_console("角色 ID 须为纯数字。")
            return
        role_id = int(tip)
        self._do_trace(enable, server_dir, role_id)

    def _do_trace(self, enable: bool, server_dir: str, role_id: int):
        cookie = self.config.get("server", "cookie", default="ddxq2-node")
        erl_path = self.config.get("erlang", "r25_path", default="")
        erl_path = erl_path if (erl_path or "").strip() else None
        node = server_dir
        rc, out, err, cmd_line = rpc_role_gs_trace_network(
            node, cookie, role_id, enable, target_ip=None, erl_path=erl_path
        )
        msg = (
            f"[Trace] 完整 RPC 命令行:\n{cmd_line}\n"
            f"[Trace] enable={enable} node={node}@{get_local_ip()} role={role_id} rc={rc}\n"
            f"{(out or err).strip()}\n"
        )
        if self.console:
            self.console.append_output(msg)

    def _toast_console(self, text: str):
        if self.console:
            self.console.append_output(f"[日志] {text}\n")


class LogSearchTab(_LogStyleMixin, QWidget):
    """在当前服务器 log 目录下 rg 递归搜索；结果在独立窗口展示。"""

    def __init__(self, config: Any, console: Any):
        super().__init__()
        self.config = config
        self.console = console
        self._standalone_window: Optional[LogViewerStandaloneDialog] = None
        self._build_ui()

    def _build_ui(self):
        root = QVBoxLayout(self)
        root.setSpacing(12)
        root.setContentsMargins(10, 10, 10, 8)

        hint = QLabel(
            "在「运行目录/run/服务器名/log」下用 ripgrep 递归搜索所有文件（含子目录，如 tag_log）。"
            "结果在独立窗口展示（与「日志搜索」中打开的阅读窗口相同，可检索行内高亮），不写入控制台。"
            "「服务器目录」与「日志搜索」子页签保持同步。"
        )
        hint.setWordWrap(True)
        hint.setStyleSheet("color: #aab4c3; font-size: 11px; padding-bottom: 4px;")
        root.addWidget(hint)

        toolbar = QFrame()
        toolbar.setStyleSheet(
            """
            QFrame#logSearchToolbar {
                background-color: #242932;
                border: 1px solid #3a4352;
                border-radius: 8px;
            }
            """
        )
        toolbar.setObjectName("logSearchToolbar")
        tb_layout = QVBoxLayout(toolbar)
        tb_layout.setSpacing(12)
        tb_layout.setContentsMargins(16, 14, 16, 14)

        row1 = QHBoxLayout()
        row1.setSpacing(12)
        row1.addWidget(self._pill_label("服务器目录"))
        self.server_combo = QComboBox()
        self.server_combo.setMinimumWidth(280)
        self.server_combo.setStyleSheet(self._combo_style())
        row1.addWidget(self.server_combo, 1)
        tb_layout.addLayout(row1)

        search_row = QHBoxLayout()
        search_row.setSpacing(10)
        search_row.addWidget(self._pill_label("rg 搜索"))
        self.file_search_input = QLineEdit()
        self.file_search_input.setPlaceholderText(
            "关键字或正则（勾选「正则表达」为 ripgrep 语法；rg 递归搜索当前服务器 log 及子目录）"
        )
        self.file_search_input.setStyleSheet(self._input_style())
        search_row.addWidget(self.file_search_input, 1)
        self.fs_case_cb = QCheckBox("区分大小写")
        search_row.addWidget(self.fs_case_cb)
        self.fs_regex_cb = QCheckBox("正则表达")
        self.fs_regex_cb.setChecked(False)
        self.fs_regex_cb.setToolTip(
            "未勾选：按字面量搜索（rg --fixed-strings）；勾选：关键字为 ripgrep 正则。"
        )
        search_row.addWidget(self.fs_regex_cb)
        self.fs_search_btn = QPushButton("搜索")
        self.fs_search_btn.setCursor(Qt.CursorShape.PointingHandCursor)
        self.fs_search_btn.setStyleSheet(self._primary_btn_style())
        self.fs_search_btn.setMinimumHeight(34)
        self.fs_search_btn.clicked.connect(self._run_file_search)
        search_row.addWidget(self.fs_search_btn)
        tb_layout.addLayout(search_row)

        root.addWidget(toolbar)

        self.search_hint = QLabel("")
        self.search_hint.setWordWrap(True)
        self.search_hint.setStyleSheet("color: #aab4c3; font-size: 11px; padding-top: 6px;")
        root.addWidget(self.search_hint)

    def refresh_server_list(self):
        if not _refresh_server_combo(self.server_combo, self.config):
            self.search_hint.setStyleSheet("color: #ff5c6a; font-size: 11px; padding-top: 6px;")
            self.search_hint.setText("未配置或无效，请在「工具设置」中设置运行目录。")
        elif not self.search_hint.text() or "运行目录" in self.search_hint.text():
            self.search_hint.setStyleSheet("color: #aab4c3; font-size: 11px; padding-top: 6px;")
            self.search_hint.setText("")

    def _open_standalone(self, title: str, content: str):
        if self._standalone_window is None:
            self._standalone_window = LogViewerStandaloneDialog(self)
        self._standalone_window.set_log(title, content)
        self._standalone_window.show()
        self._standalone_window.raise_()
        self._standalone_window.activateWindow()

    def _run_file_search(self):
        if not find_rg_executable():
            QMessageBox.warning(
                self,
                "全局搜索",
                "未找到 rg.exe。\n请将 ripgrep 的 rg.exe 放在 Server Manager 程序目录，"
                "或设置环境变量 RG_PATH 指向 rg 可执行文件。",
            )
            return
        run_dir = _run_dir_from_config(self.config)
        if not run_dir or not Path(run_dir).exists():
            QMessageBox.warning(self, "全局搜索", "运行目录无效。")
            return
        server_dir = self.server_combo.currentText()
        if server_dir.startswith("("):
            QMessageBox.warning(self, "全局搜索", "请先选择有效的服务器目录。")
            return
        pattern = (self.file_search_input.text() or "").strip()
        if not pattern:
            QMessageBox.information(self, "全局搜索", "请输入搜索关键字。")
            return
        log_root = Path(run_dir) / server_dir / "log"
        if not log_root.is_dir():
            QMessageBox.warning(self, "全局搜索", f"目录不存在：\n{log_root}")
            return
        out, err = rg_search_directory(
            log_root,
            pattern,
            case_sensitive=self.fs_case_cb.isChecked(),
            use_regex=self.fs_regex_cb.isChecked(),
            whole_word=False,
        )
        if err:
            QMessageBox.warning(self, "全局搜索", err)
            return
        title = f"全局搜索 — {server_dir} {pattern!r} ({log_root})"
        self._open_standalone(title, out)
        self.search_hint.setStyleSheet("color: #25c889; font-size: 11px;")
        self.search_hint.setText("rg 搜索结果已在新窗口打开（与「日志搜索」阅读窗口相同，可检索行内高亮）。")


class LogViewerTab(QWidget):
    """顶级「日志查看」：子页签「日志搜索」(单文件) +「全局搜索」(rg 目录)。"""

    def __init__(self, config: Any, console: Any):
        super().__init__()
        self.config = config
        self.console = console
        self.load_tab = LogLoadTab(config, console)
        self.search_tab = LogSearchTab(config, console)
        lay = QVBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(0)
        sub = QTabWidget()
        sub.setDocumentMode(True)
        sub.setObjectName("logSubTabs")
        sub.setStyleSheet(
            """
            QTabWidget#logSubTabs::pane {
                border: none;
                background-color: #181b22;
                top: 0px;
            }
            QTabWidget#logSubTabs QTabBar::tab {
                background-color: #242932;
                color: #c6d7ef;
                border: 1px solid #3a4352;
                border-radius: 7px;
                padding: 8px 18px;
                min-width: 82px;
                margin: 8px 6px 8px 0px;
                font-size: 13px;
                font-weight: 600;
            }
            QTabWidget#logSubTabs QTabBar::tab:selected {
                background-color: #4f8cff;
                color: #e8eef7;
                border-color: #4f8cff;
            }
            QTabWidget#logSubTabs QTabBar::tab:hover:!selected {
                background-color: #263a55;
                color: #4f8cff;
                border-color: #4f6b92;
            }
            QTabWidget#logSubTabs QTabBar::tab:pressed {
                background-color: #2b4d75;
            }
            """
        )
        sub.addTab(self.load_tab, "日志搜索")
        sub.addTab(self.search_tab, "全局搜索")
        lay.addWidget(sub)
        self._link_server_combos()
        QTimer.singleShot(0, self.refresh_server_list)

    def _link_server_combos(self):
        self._log_combo_syncing = False

        def _sync(src: QComboBox, dst: QComboBox):
            if self._log_combo_syncing:
                return
            self._log_combo_syncing = True
            dst.blockSignals(True)
            dst.setCurrentIndex(src.currentIndex())
            dst.blockSignals(False)
            self._log_combo_syncing = False

        a = self.load_tab.server_combo
        b = self.search_tab.server_combo
        a.currentIndexChanged.connect(lambda: _sync(a, b))
        b.currentIndexChanged.connect(lambda: _sync(b, a))

    def refresh_server_list(self):
        self.load_tab.refresh_server_list()
        self.search_tab.refresh_server_list()
