# -*- coding: utf-8 -*-
"""Main tab views."""

from util.runtime import *
from util.utils import node_host_only, _server_id_sort_key
from models.config import Config, apply_terminal_launch_config
from controllers.workers import (
    CommandRunner,
    GroupedNodeStatusCheckWorker,
    NodeStatusCheckWorker,
    StopServerWorker,
)
from views.common import (
    NoWheelComboBox,
    NoWheelDateTimeEdit,
    NoWheelSpinBox,
    ServerIdTableItem,
    apply_button_style,
    make_line_icon,
    set_button_icon,
)
from views.console import OutputConsole
from views.dialogs import (
    ClearDatabaseDialog,
    KvConfigEditDialog,
    RemoteConnectDialog,
    ServerSelectDialog,
    ViewAccountsDialog,
)

class ConfigTab(QWidget):
    """配置页面"""
    
    # 配置保存信号
    config_saved = pyqtSignal()
    
    def __init__(self, config: Config, console: OutputConsole = None):
        super().__init__()
        self.config = config
        self.console = console
        self.default_kv = {}
        self._load_default_kv()
        self.init_ui()
    
    def _load_default_kv(self):
        """读取合并配置（user.config > project.config > default.config）"""
        server_root = self.config.get('workspace', 'server_root', default='')
        if not server_root:
            return
        
        self.default_kv = read_merged_config(server_root)
    
    def _get_default(self, key: str, fallback: str = '') -> str:
        """获取默认值，从合并配置读取"""
        value = self.default_kv.get(key, fallback)
        # 去掉可能的引号
        if isinstance(value, str) and value.startswith("'") and value.endswith("'"):
            value = value[1:-1]
        return value
    
    def _get_config_value(self, config_keys: tuple, default_kv_key: str, fallback=''):
        """获取配置值：优先从 user.config 读取，没有值则从 project.config/default.config 读取
        
        Args:
            config_keys: user.config 的键路径，如 ('database', 'host')
            default_kv_key: default.config 中的键名，如 'db_host'
            fallback: 最终的默认值
        """
        # 先从 user.config 读取
        value = self.config.get(*config_keys, default=None)
        
        # 如果为空，从 project.config/default.config 读取
        if value is None or value == '' or value == 0:
            value = self._get_default(default_kv_key, str(fallback) if fallback else '')
        
        return value if value else fallback
    
    def init_ui(self):
        layout = QVBoxLayout(self)
        layout.setContentsMargins(10, 10, 10, 8)
        layout.setSpacing(10)

        default_db_host = self._get_config_value(('database', 'host'), 'db_host', '')
        default_db_user = self._get_config_value(('database', 'user'), 'db_user', '')
        default_db_pass = self._get_config_value(('database', 'password'), 'db_pass', '')
        default_db_port = self._get_config_value(('database', 'port'), 'db_port', 3306)
        default_prefix = self._get_config_value(('server', 'prefix'), 'prefix', 'ddxq2')
        default_server_id = self._get_config_value(('server', 'default_server_id'), 'server_id', '1')
        default_server_type = self._get_config_value(('server', 'server_type'), 'server_type', 'game_server')
        default_server_name = self._get_config_value(('server', 'server_name'), 'server_name', '')
        default_login_node = self._get_config_value(('server', 'login_server_node'), 'login_node', '')
        default_center_node = self._get_config_value(('server', 'center_server_node'), 'center_node', '')
        default_cookie = self._get_config_value(('server', 'cookie'), 'cookie', 'ddxq2-node')
        default_tcp_port = int(self.default_kv.get('tcp_port', self.default_kv.get('game_tcp_port', '18100')) or 18100)
        default_http_port = int(self.default_kv.get('http_port', '19100') or 19100)
        default_r25_path = self._get_config_value(('erlang', 'r25_path'), 'r25_path', '')
        default_terminal_mode = self.config.get('terminal', 'launch_mode', default='auto') or 'auto'
        default_terminal_exe = self.config.get('terminal', 'exe_path', default='') or ''

        def add_browse_row(form: QFormLayout, label: str, line_edit: QLineEdit):
            row = QHBoxLayout()
            row.setContentsMargins(0, 0, 0, 0)
            row.setSpacing(8)
            row.addWidget(line_edit, 1)
            btn = QPushButton("浏览")
            btn.setFixedWidth(54)
            apply_button_style(btn, "outline", compact=True)
            set_button_icon(btn, "folder", "#4f8cff", 14)
            btn.clicked.connect(lambda: self._browse_dir(line_edit))
            row.addWidget(btn)
            form.addRow(label, row)
            return btn

        def add_browse_file_row(form: QFormLayout, label: str, line_edit: QLineEdit):
            row = QHBoxLayout()
            row.setContentsMargins(0, 0, 0, 0)
            row.setSpacing(8)
            row.addWidget(line_edit, 1)
            btn = QPushButton("浏览")
            btn.setFixedWidth(54)
            apply_button_style(btn, "outline", compact=True)
            set_button_icon(btn, "folder", "#4f8cff", 14)
            btn.clicked.connect(lambda: self._browse_file(line_edit))
            row.addWidget(btn)
            form.addRow(label, row)
            return btn

        def add_readonly_row(form: QFormLayout, label: str, line_edit: QLineEdit):
            line_edit.setReadOnly(True)
            line_edit.setToolTip("由当前打开的项目目录决定")
            line_edit.setCursorPosition(0)
            form.addRow(label, line_edit)

        def make_form_group(title: str, icon_kind: str = "settings") -> tuple[QFrame, QFormLayout]:
            group = QFrame()
            group.setObjectName("settingsFormCard")
            group.setStyleSheet(
                """
                QFrame#settingsFormCard {
                    background-color: #242932;
                    border: 1px solid #3a4352;
                    border-radius: 8px;
                }
                """
            )
            outer = QVBoxLayout(group)
            outer.setContentsMargins(18, 14, 18, 16)
            outer.setSpacing(16)

            header = QHBoxLayout()
            header.setContentsMargins(0, 0, 0, 0)
            header.setSpacing(8)
            icon = QLabel()
            icon.setPixmap(make_line_icon(icon_kind, "#4f8cff", 18).pixmap(18, 18))
            header.addWidget(icon)
            label = QLabel(title)
            label.setStyleSheet("color: #e8eef7; font-size: 14px; font-weight: 700;")
            header.addWidget(label)
            header.addStretch()
            outer.addLayout(header)

            body = QWidget()
            form = QFormLayout(body)
            form.setSpacing(10)
            form.setContentsMargins(0, 0, 0, 0)
            form.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
            form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
            outer.addWidget(body, 1)
            return group, form

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFrameShape(QFrame.Shape.NoFrame)
        content = QWidget()
        content_layout = QVBoxLayout(content)
        content_layout.setContentsMargins(0, 0, 0, 0)
        content_layout.setSpacing(10)

        # 项目目录配置
        project_group, project_layout = make_form_group("项目目录配置", "file")
        self.server_root = QLineEdit(self.config.get('workspace', 'server_root', default=''))
        self.run_dir = QLineEdit(self.config.get('workspace', 'run_dir', default=''))
        add_readonly_row(project_layout, "服务器根目录：", self.server_root)
        add_readonly_row(project_layout, "运行目录：", self.run_dir)
        content_layout.addWidget(project_group)

        # 启动窗口配置
        terminal_group, terminal_layout = make_form_group("启动窗口配置", "external")
        self.terminal_launch_mode = NoWheelComboBox()
        for text, value in (
            ("自动", "auto"),
            ("cmd", "cmd"),
            ("PowerShell", "powershell"),
            ("其他", "other"),
        ):
            self.terminal_launch_mode.addItem(text, value)
        terminal_index = self.terminal_launch_mode.findData(default_terminal_mode)
        if terminal_index < 0:
            terminal_index = 0
        self.terminal_launch_mode.setCurrentIndex(terminal_index)
        self.terminal_exe_path = QLineEdit(default_terminal_exe)
        self.terminal_exe_path.setToolTip("选择终端程序 exe；启动时会自动生成并传入对应启动命令")
        terminal_layout.addRow("启动方式：", self.terminal_launch_mode)
        self.terminal_exe_path_btn = add_browse_file_row(
            terminal_layout,
            "运行 EXE：",
            self.terminal_exe_path,
        )
        self.terminal_launch_mode.currentIndexChanged.connect(self._update_terminal_exe_fields)
        content_layout.addWidget(terminal_group)

        # 数据库配置
        db_group, db_layout = make_form_group("数据库配置", "database")
        self.db_host = QLineEdit(default_db_host)
        self.db_port = NoWheelSpinBox()
        self.db_port.setRange(1, 65535)
        self.db_port.setValue(int(default_db_port) if default_db_port else 3306)
        self.db_user = QLineEdit(default_db_user)
        self.db_pass = self._create_password_field(default_db_pass)
        db_layout.addRow("数据库主机：", self.db_host)
        db_layout.addRow("数据库端口：", self.db_port)
        db_layout.addRow("数据库用户：", self.db_user)
        db_layout.addRow("数据库密码：", self.db_pass)
        db_test_row = QHBoxLayout()
        db_test_row.setContentsMargins(0, 0, 0, 0)
        db_test_row.addStretch()
        self.test_db_btn = QPushButton("测试连接")
        apply_button_style(self.test_db_btn, "outline", compact=True)
        set_button_icon(self.test_db_btn, "link", "#4f8cff", 14)
        self.test_db_btn.clicked.connect(self._test_database_connection)
        db_test_row.addWidget(self.test_db_btn)
        db_layout.addRow("", db_test_row)
        content_layout.addWidget(db_group)

        # 服务器默认配置
        srv_group, srv_layout = make_form_group("服务器默认配置", "server")
        self.prefix = QLineEdit(default_prefix)
        self.default_id = QLineEdit(default_server_id)
        self.server_type = NoWheelComboBox()
        self.server_type.addItems(['game_server', 'login_server', 'center_server', 'cross_server', 'client_server'])
        self.server_type.setCurrentText(default_server_type)
        self.server_name = QLineEdit(default_server_name)
        self.default_tcp_port = NoWheelSpinBox()
        self.default_tcp_port.setRange(1, 65535)
        self.default_tcp_port.setValue(default_tcp_port)
        self.default_http_port = NoWheelSpinBox()
        self.default_http_port.setRange(1, 65535)
        self.default_http_port.setValue(default_http_port)
        srv_layout.addRow("服务器前缀：", self.prefix)
        srv_layout.addRow("默认服务器 ID：", self.default_id)
        srv_layout.addRow("服务器类型：", self.server_type)
        srv_layout.addRow("默认服务器名：", self.server_name)
        srv_layout.addRow("默认 TCP 端口：", self.default_tcp_port)
        srv_layout.addRow("默认 HTTP 端口：", self.default_http_port)
        content_layout.addWidget(srv_group)

        # Erlang 与节点配置
        erlang_group, erlang_layout = make_form_group("Erlang 与节点配置", "network")
        self.r25_path = QLineEdit(default_r25_path)
        add_browse_row(erlang_layout, "Erlang 路径：", self.r25_path)

        self.erl_version = QLineEdit()
        self.erl_version.setReadOnly(True)
        version_row = QHBoxLayout()
        version_row.setContentsMargins(0, 0, 0, 0)
        version_row.setSpacing(8)
        version_row.addWidget(self.erl_version, 1)
        check_erl_btn = QPushButton("检查版本")
        apply_button_style(check_erl_btn, "outline", compact=True)
        set_button_icon(check_erl_btn, "check", "#4f8cff", 14)
        check_erl_btn.clicked.connect(self._check_env_erl_version)
        version_row.addWidget(check_erl_btn)
        erlang_layout.addRow("Erlang 版本：", version_row)

        self.login_node = QLineEdit(default_login_node)
        self.center_node = QLineEdit(default_center_node)
        self.cookie = QLineEdit(default_cookie)

        login_row = QHBoxLayout()
        login_row.setContentsMargins(0, 0, 0, 0)
        login_row.setSpacing(8)
        login_row.addWidget(self.login_node, 1)
        self.test_login_node_btn = QPushButton("测试连接")
        apply_button_style(self.test_login_node_btn, "outline", compact=True)
        set_button_icon(self.test_login_node_btn, "link", "#4f8cff", 14)
        self.test_login_node_btn.clicked.connect(self._test_login_node_connection)
        login_row.addWidget(self.test_login_node_btn)
        erlang_layout.addRow("登录服节点：", login_row)

        center_row = QHBoxLayout()
        center_row.setContentsMargins(0, 0, 0, 0)
        center_row.setSpacing(8)
        center_row.addWidget(self.center_node, 1)
        self.test_center_node_btn = QPushButton("测试连接")
        apply_button_style(self.test_center_node_btn, "outline", compact=True)
        set_button_icon(self.test_center_node_btn, "link", "#4f8cff", 14)
        self.test_center_node_btn.clicked.connect(self._test_center_node_connection)
        center_row.addWidget(self.test_center_node_btn)
        erlang_layout.addRow("中心服节点：", center_row)
        erlang_layout.addRow("Cookie：", self.cookie)
        content_layout.addWidget(erlang_group)

        content_layout.addStretch()
        scroll.setWidget(content)
        layout.addWidget(scroll, 1)

        actions = QHBoxLayout()
        actions.addStretch()
        fill_default_btn = QPushButton("填充默认值")
        fill_default_btn.clicked.connect(self._fill_default_values)
        apply_button_style(fill_default_btn, "success")
        set_button_icon(fill_default_btn, "download", "#242932", 16)
        save_btn = QPushButton("保存配置")
        save_btn.clicked.connect(self.save_config)
        apply_button_style(save_btn, "primary")
        set_button_icon(save_btn, "save", "#242932", 16)
        actions.addWidget(fill_default_btn)
        actions.addWidget(save_btn)
        layout.addLayout(actions)
        self._update_terminal_exe_fields()
        QTimer.singleShot(0, self._update_erl_version)
    
    def _create_password_field(self, default_value: str = '') -> QLineEdit:
        """创建带白色眼睛图标的密码输入框"""
        from PyQt6.QtGui import QPixmap, QPainter
        
        password_input = QLineEdit(default_value)
        password_input.setEchoMode(QLineEdit.EchoMode.Password)
        
        # 创建白色眼睛图标（简单圆形+点表示眼睛）
        pixmap = QPixmap(16, 16)
        pixmap.fill(Qt.GlobalColor.transparent)
        painter = QPainter(pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        pen = painter.pen()
        pen.setColor(QColor(200, 200, 200))  # 浅灰白色
        pen.setWidth(2)
        painter.setPen(pen)
        # 画眼睛轮廓（椭圆）
        painter.drawEllipse(2, 4, 12, 8)
        # 画瞳孔（实心圆）
        painter.setBrush(QColor(200, 200, 200))
        painter.drawEllipse(6, 6, 4, 4)
        painter.end()
        
        icon = QIcon(pixmap)
        toggle_action = QAction(icon, "", password_input)
        toggle_action.setToolTip("显示/隐藏密码")
        password_input.addAction(toggle_action, QLineEdit.ActionPosition.TrailingPosition)
        toggle_action.triggered.connect(lambda: self._toggle_password_visibility(password_input))
        
        return password_input
    
    def _toggle_password_visibility(self, line_edit):
        """切换密码显示/隐藏"""
        if line_edit.echoMode() == QLineEdit.EchoMode.Password:
            line_edit.setEchoMode(QLineEdit.EchoMode.Normal)
        else:
            line_edit.setEchoMode(QLineEdit.EchoMode.Password)
    
    def _browse_dir(self, line_edit):
        """浏览目录"""
        dir_path = QFileDialog.getExistingDirectory(self, "选择目录", line_edit.text())
        if dir_path:
            line_edit.setText(dir_path)

    def _browse_file(self, line_edit):
        """浏览文件"""
        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "选择程序",
            line_edit.text(),
            "程序 (*.exe);;所有文件 (*)",
        )
        if file_path:
            line_edit.setText(file_path)

    def _update_terminal_exe_fields(self):
        mode = self.terminal_launch_mode.currentData() or self.terminal_launch_mode.currentText()
        enabled = mode == 'other'
        for widget in (
            self.terminal_exe_path,
            self.terminal_exe_path_btn,
        ):
            widget.setEnabled(enabled)
    
    def _update_erl_version(self):
        """更新 Erlang 版本显示"""
        import subprocess
        
        erl_path = self.r25_path.text().strip()
        
        # 确定 erl 命令路径
        if erl_path:
            erl_bin = Path(erl_path) / 'bin' / 'erl'
            if IS_WINDOWS:
                erl_bin = Path(erl_path) / 'bin' / 'erl.exe'
            if not erl_bin.exists():
                self.erl_version.setText("路径无效")
                return
            erl_cmd = str(erl_bin)
        else:
            erl_cmd = 'erl'
        
        try:
            # 使用 erl -eval 获取版本
            result = subprocess.run(
                [erl_cmd, '-eval', 'io:format("~s", [erlang:system_info(otp_release)]), halt().', '-noshell'],
                capture_output=True,
                text=True,
                timeout=5
            )
            if result.returncode == 0 and result.stdout:
                version = result.stdout.strip()
                source = "配置路径" if erl_path else "系统环境"
                self.erl_version.setText(f"OTP {version} ({source})")
            else:
                self.erl_version.setText("获取失败")
        except FileNotFoundError:
            self.erl_version.setText("未找到 erl" if not erl_path else "路径无效")
        except subprocess.TimeoutExpired:
            self.erl_version.setText("获取超时")
        except Exception as e:
            self.erl_version.setText(f"错误: {str(e)[:20]}")
    
    def _log(self, msg: str):
        """输出日志到控制台"""
        if self.console:
            self.console.append_output(msg)
    
    def _check_env_erl_version(self):
        """检查当前运行环境的 Erlang 版本"""
        import subprocess
        from datetime import datetime
        
        def ts():
            return datetime.now().strftime('%H:%M:%S')
        
        self._log(f"\n[{ts()}] ===== 检查 Erlang 环境 =====\n")
        
        try:
            # 获取系统环境的 erl 版本
            self._log(f"[{ts()}] 正在检查系统环境 Erlang 版本...\n")
            result = subprocess.run(
                ['erl', '-eval', 'io:format("~s", [erlang:system_info(otp_release)]), halt().', '-noshell'],
                capture_output=True,
                text=True,
                timeout=5
            )
            
            if result.returncode == 0 and result.stdout:
                env_version = result.stdout.strip()
                self._log(f"[{ts()}] 系统环境 Erlang 版本: OTP {env_version}\n")
                
                # 获取配置路径的版本进行对比
                erl_path = self.r25_path.text().strip()
                config_version = None
                
                if erl_path:
                    self._log(f"[{ts()}] 配置路径: {erl_path}\n")
                    erl_bin = Path(erl_path) / 'bin' / 'erl'
                    if IS_WINDOWS:
                        erl_bin = Path(erl_path) / 'bin' / 'erl.exe'
                    if erl_bin.exists():
                        self._log(f"[{ts()}] 正在检查配置路径 Erlang 版本...\n")
                        cfg_result = subprocess.run(
                            [str(erl_bin), '-eval', 'io:format("~s", [erlang:system_info(otp_release)]), halt().', '-noshell'],
                            capture_output=True,
                            text=True,
                            timeout=5
                        )
                        if cfg_result.returncode == 0 and cfg_result.stdout:
                            config_version = cfg_result.stdout.strip()
                            self._log(f"[{ts()}] 配置路径 Erlang 版本: OTP {config_version}\n")
                    else:
                        self._log(f"[{ts()}] [警告] 配置路径无效: {erl_bin}\n")
                else:
                    self._log(f"[{ts()}] 未配置 Erlang 路径，将使用系统环境\n")
                
                if config_version and config_version != env_version:
                    self._log(f"[{ts()}] [警告] 版本不一致!\n")
                    self._log(f"[{ts()}] - 系统环境: OTP {env_version}\n")
                    self._log(f"[{ts()}] - 配置路径: OTP {config_version}\n")
                    self._log(f"[{ts()}] 执行 rebar3 编译时将使用配置路径版本 (OTP {config_version})\n")
                    QMessageBox.warning(
                        self, "版本不一致",
                        f"当前环境 Erlang 版本: OTP {env_version}\n"
                        f"配置路径 Erlang 版本: OTP {config_version}\n\n"
                        f"执行 rebar3 编译时将使用配置路径版本 (OTP {config_version})"
                    )
                else:
                    if config_version:
                        self._log(f"[{ts()}] [成功] 环境版本与配置版本一致: OTP {env_version}\n")
                        QMessageBox.information(self, "版本一致", f"环境版本与配置版本一致: OTP {env_version}")
                    else:
                        self._log(f"[{ts()}] [信息] 当前使用系统环境 Erlang 版本: OTP {env_version}\n")
                        QMessageBox.information(self, "环境版本", f"当前环境 Erlang 版本: OTP {env_version}")
            else:
                self._log(f"[{ts()}] [错误] 获取系统环境 Erlang 版本失败\n")
        except FileNotFoundError:
            self._log(f"[{ts()}] [错误] 系统环境变量中未找到 erl 命令\n")
            self._log(f"[{ts()}] 请将 Erlang 的 bin 目录添加到 PATH 环境变量中\n")
            QMessageBox.warning(self, "未找到 Erlang", "系统环境变量中未找到 erl 命令。\n请将 Erlang 的 bin 目录添加到 PATH 环境变量中。")
        except subprocess.TimeoutExpired:
            self._log(f"[{ts()}] [错误] 获取 Erlang 版本超时\n")
        except Exception as e:
            self._log(f"[{ts()}] [错误] {str(e)}\n")
    
    def _create_password_input(self, default_value: str = '') -> QLineEdit:
        """创建带眼睛图标的密码输入框"""
        password_input = QLineEdit(default_value)
        password_input.setEchoMode(QLineEdit.EchoMode.Password)
        
        # 使用 QAction 在输入框右侧添加眼睛图标
        toggle_action = QAction(make_line_icon("eye", "#778398", 16), "", password_input)
        toggle_action.setToolTip("显示/隐藏密码")
        password_input.addAction(toggle_action, QLineEdit.ActionPosition.TrailingPosition)
        
        # 保存状态
        password_input.setProperty("password_visible", False)
        
        def toggle_visibility():
            visible = password_input.property("password_visible")
            if visible:
                password_input.setEchoMode(QLineEdit.EchoMode.Password)
                toggle_action.setIcon(make_line_icon("eye", "#778398", 16))
                password_input.setProperty("password_visible", False)
            else:
                password_input.setEchoMode(QLineEdit.EchoMode.Normal)
                toggle_action.setIcon(make_line_icon("eye_off", "#778398", 16))
                password_input.setProperty("password_visible", True)
        
        toggle_action.triggered.connect(toggle_visibility)
        
        return password_input
    
    def _test_login_node_connection(self):
        """测试登录服 Erlang 节点是否可达"""
        self._test_erlang_node_connection(self.login_node.text().strip(), "登录服")
    
    def _test_center_node_connection(self):
        """测试中心服 Erlang 节点是否可达"""
        self._test_erlang_node_connection(self.center_node.text().strip(), "中心服")
    
    def _test_erlang_node_connection(self, node: str, kind_label: str):
        """使用 net_adm:ping 检测节点（需与本页 Cookie、Erlang 路径一致）"""
        if not node:
            QMessageBox.warning(self, "提示", f"请先填写{kind_label}节点（形如 name@ip）")
            return
        cookie = self.cookie.text().strip()
        if not cookie:
            QMessageBox.warning(self, "提示", "请先填写 Cookie（与目标节点 erlang.cookie 一致）")
            return
        erl_path = self.r25_path.text().strip() or None
        self.test_login_node_btn.setEnabled(False)
        self.test_center_node_btn.setEnabled(False)
        QApplication.processEvents()
        try:
            ok = check_node_status(node, cookie, erl_path=erl_path)
        except Exception as e:
            QMessageBox.warning(self, "测试异常", str(e))
            ok = False
        finally:
            self.test_login_node_btn.setEnabled(True)
            self.test_center_node_btn.setEnabled(True)
        if ok:
            QMessageBox.information(self, "连接成功", f"{kind_label}节点可达：\n{node}")
        else:
            QMessageBox.warning(
                self, "连接失败",
                f"无法连通{kind_label}节点：\n{node}\n\n"
                "请核对节点名、Cookie、网络与防火墙，并确认上方 Erlang 路径可用。"
            )

    def _test_database_connection(self):
        """测试当前表单中的 MySQL 连接是否可用。"""
        host = self.db_host.text().strip()
        port = self.db_port.value()
        user = self.db_user.text().strip()
        password = self.db_pass.text()

        if not host:
            QMessageBox.warning(self, "提示", "请先填写数据库主机")
            return
        if not user:
            QMessageBox.warning(self, "提示", "请先填写数据库用户")
            return

        self.test_db_btn.setEnabled(False)
        old_text = self.test_db_btn.text()
        self.test_db_btn.setText("测试中...")
        QApplication.processEvents()

        try:
            result = test_mysql_connection(host, port, user, password, timeout=5)
            version = result.get("server_version") or "未知"
            current_user = result.get("current_user") or user
            self._log(f"[数据库] MySQL 连接成功: {host}:{port}, user={current_user}, version={version}\n")
            QMessageBox.information(
                self,
                "连接成功",
                f"MySQL 连接成功：\n{host}:{port}\n\n"
                f"版本：{version}\n"
                f"当前用户：{current_user}",
            )
        except ImportError as e:
            QMessageBox.critical(self, "缺少依赖", str(e))
        except Exception as e:
            self._log(f"[数据库] MySQL 连接失败: {host}:{port}, error={e}\n")
            QMessageBox.warning(self, "连接失败", f"无法连接 MySQL：\n{e}")
        finally:
            self.test_db_btn.setText(old_text)
            self.test_db_btn.setEnabled(True)
    
    def _fill_default_values(self):
        """从 default.config 填充默认值到表单"""
        server_root = self.server_root.text()
        if not server_root:
            QMessageBox.warning(self, "警告", "请先设置服务器根目录")
            return
        
        default_kv_path = find_existing_default_config_path(server_root)
        if not default_kv_path:
            expected_path = get_default_config_path(server_root)
            QMessageBox.warning(self, "警告", 
                f"找不到默认配置文件:\n{expected_path}\n\n请检查工作路径是否正确")
            return
        
        # 重新加载 default.config
        self.default_kv = read_config_file(str(default_kv_path))
        
        if not self.default_kv:
            QMessageBox.warning(self, "警告", "default.config 文件为空或格式错误")
            return
        
        # 填充数据库配置
        if 'db_host' in self.default_kv:
            self.db_host.setText(self.default_kv['db_host'])
        if 'db_user' in self.default_kv:
            self.db_user.setText(self.default_kv['db_user'])
        if 'db_pass' in self.default_kv:
            self.db_pass.setText(self.default_kv['db_pass'])
        if 'db_port' in self.default_kv:
            try:
                self.db_port.setValue(int(self.default_kv['db_port']))
            except ValueError:
                pass
        
        # 填充服务器配置
        if 'prefix' in self.default_kv:
            self.prefix.setText(self.default_kv['prefix'])
        if 'server_id' in self.default_kv:
            self.default_id.setText(self.default_kv['server_id'])
        if 'server_type' in self.default_kv:
            self.server_type.setCurrentText(self.default_kv['server_type'])
        if 'server_name' in self.default_kv:
            self.server_name.setText(self.default_kv['server_name'])
        if 'login_node' in self.default_kv:
            val = self.default_kv['login_node']
            if val.startswith("'") and val.endswith("'"):
                val = val[1:-1]
            self.login_node.setText(val)
        if 'center_node' in self.default_kv:
            val = self.default_kv['center_node']
            if val.startswith("'") and val.endswith("'"):
                val = val[1:-1]
            self.center_node.setText(val)
        if 'cookie' in self.default_kv:
            self.cookie.setText(self.default_kv['cookie'])
        
        QMessageBox.information(self, "成功", "已从 default.config 填充默认值\n\n请检查后点击「保存配置」")
    
    def save_config(self):
        """保存配置到 .server_manager/config/user.config"""
        # 保存前记录当前 Erlang 路径，用于判断版本是否变更
        old_r25_path = (self.config.get('erlang', 'r25_path', default='') or '').strip()
        new_r25_path = self.r25_path.text().strip()
        erl_version_changed = (old_r25_path != new_r25_path)
        terminal_mode = self.terminal_launch_mode.currentData() or self.terminal_launch_mode.currentText()
        terminal_exe_path = self.terminal_exe_path.text().strip()

        if terminal_mode == 'other':
            if not terminal_exe_path:
                QMessageBox.warning(self, "警告", "启动方式为“其他”时，请选择运行 EXE")
                return
            expanded_exe = Path(os.path.expandvars(os.path.expanduser(terminal_exe_path.strip('"'))))
            is_path_like = expanded_exe.is_absolute() or any(sep in terminal_exe_path for sep in ('/', '\\'))
            if is_path_like:
                if not expanded_exe.is_file():
                    QMessageBox.warning(self, "警告", f"运行 EXE 不存在:\n{terminal_exe_path}")
                    return
            elif shutil.which(terminal_exe_path) is None:
                QMessageBox.warning(self, "警告", f"未在 PATH 中找到运行 EXE:\n{terminal_exe_path}")
                return

        self.config.set(self.db_host.text(), 'database', 'host')
        self.config.set(self.db_user.text(), 'database', 'user')
        self.config.set(self.db_pass.text(), 'database', 'password')
        self.config.set(self.db_port.value(), 'database', 'port')
        
        # 服务器配置
        self.config.set(self.prefix.text(), 'server', 'prefix')
        self.config.set(self.default_id.text(), 'server', 'default_server_id')
        self.config.set(self.server_type.currentText(), 'server', 'server_type')
        self.config.set(self.server_name.text(), 'server', 'server_name')
        self.config.set(self.login_node.text(), 'server', 'login_server_node')
        self.config.set(self.center_node.text(), 'server', 'center_server_node')
        self.config.set(self.cookie.text(), 'server', 'cookie')
        
        self.config.set(self.r25_path.text(), 'erlang', 'r25_path')
        self.config.set(terminal_mode, 'terminal', 'launch_mode')
        self.config.set(terminal_exe_path, 'terminal', 'exe_path')
        
        self.config.save()
        apply_terminal_launch_config(self.config)
        
        # 若修改了 Erlang 版本，弹框询问是否清除 _build 并重新编译
        if erl_version_changed:
            reply = QMessageBox.question(
                self,
                "Erlang 版本已修改",
                "修改了 Erlang 版本，需要将本项目的 _build 目录删除并重新编译。\n\n是否立即清除 _build 并编译？",
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No
            )
            if reply == QMessageBox.StandardButton.Yes:
                import shutil
                server_root = self.config.get('workspace', 'server_root', default='') or (
                    str(self.config.tool_dir) if self.config.tool_dir else str(Path.cwd())
                )
                build_dir = Path(server_root) / "_build"
                if build_dir.exists():
                    try:
                        shutil.rmtree(build_dir)
                        self.console.append_output(f"[配置] 已删除 _build 目录: {build_dir}\n")
                    except Exception as e:
                        self.console.append_output(f"[配置] 删除 _build 失败: {e}\n")
                        QMessageBox.warning(self, "警告", f"删除 _build 目录失败:\n{e}")
                erl_path = self.config.get('erlang', 'r25_path', default='')
                rebar3 = get_rebar3_cmd(server_root, erl_path)
                cmd = f'{rebar3} as game_server_dev compile'
                runner = CommandRunner(cmd, cwd=server_root, shell=True, start_new_window=False)
                runner.output_signal.connect(self.console.append_output)
                runner.start()
                self.console.append_output("[配置] 已开始重新编译，请查看下方输出。\n")
        
        # 发出配置保存信号，通知其他页签刷新
        self.config_saved.emit()
        
        QMessageBox.information(self, "成功", "项目配置已保存到当前项目的 .server_manager/config/user.config")
