# -*- coding: utf-8 -*-
"""
MARS 轴向磁拉力仿真工具 — PyQt5 GUI (现代工业风 v2)
====================================================
双击运行 (或 python main.py), 调整参数后点击运行, 无需 AI token。
"""
import os
import sys
import json
import time
import traceback

from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QSize
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QGridLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QCheckBox,
    QTabWidget, QTextEdit, QGroupBox, QDoubleSpinBox, QSpinBox,
    QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, QSplitter,
    QFrame, QSizePolicy, QStatusBar, QScrollArea
)
from PyQt5.QtGui import QFont, QColor, QPalette

BASE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_MOT = os.path.normpath(
    os.path.join(BASE, "..", "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot"))
DEFAULT_OUT = os.path.normpath(os.path.join(BASE, "..", "output_motorcad"))
DEFAULT_MC_EXE = r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe"

# ============================================================
# 现代工业风 QSS — 浅灰蓝底 + 白色卡片 + 工业蓝强调
# ============================================================
STYLE = """
* { font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 10pt; }

QMainWindow { background: #eef1f5; }

/* --- 卡片式分组 --- */
QGroupBox {
    font-weight: 600; font-size: 11pt; color: #1e293b;
    border: 1px solid #e2e8f0; border-radius: 8px;
    margin-top: 16px; padding-top: 14px; padding-left: 10px; padding-right: 10px;
    background: #ffffff;
}
QGroupBox::title {
    subcontrol-origin: margin; left: 14px; padding: 0 8px;
    background: #ffffff; color: #1e293b;
}

/* --- 标签 --- */
QLabel { color: #334155; font-size: 10pt; }
QLabel#cardValue { font-size: 22pt; font-weight: 700; color: #1e293b; }
QLabel#cardUnit  { font-size: 11pt; color: #64748b; font-weight: 400; }
QLabel#cardLabel { font-size: 9pt; color: #94a3b8; font-weight: 500; letter-spacing: 1px; }
QLabel#sectionTitle { font-size: 11pt; font-weight: 600; color: #1e293b; padding-bottom: 4px; }

/* --- 输入框 --- */
QLineEdit, QDoubleSpinBox, QSpinBox {
    padding: 6px 10px; border: 1px solid #cbd5e1; border-radius: 6px;
    background: #f8fafc; color: #1e293b; font-size: 10pt; min-height: 22px;
    selection-background-color: #2563eb; selection-color: white;
}
QLineEdit:focus, QDoubleSpinBox:focus, QSpinBox:focus {
    border: 1px solid #2563eb; background: #ffffff;
}
QDoubleSpinBox::up-button, QSpinBox::up-button,
QDoubleSpinBox::down-button, QSpinBox::down-button { width: 18px; }

/* --- 按钮 --- */
QPushButton {
    padding: 8px 20px; border-radius: 6px; font-size: 10pt; font-weight: 600;
    border: 1px solid #2563eb; background: #2563eb; color: white;
}
QPushButton:hover { background: #1d4ed8; border-color: #1d4ed8; }
QPushButton:pressed { background: #1e40af; }
QPushButton:disabled { background: #94a3b8; border-color: #94a3b8; color: #e2e8f0; }
QPushButton#secondary { background: #ffffff; color: #2563eb; border: 1px solid #cbd5e1; }
QPushButton#secondary:hover { background: #f1f5f9; border-color: #2563eb; }
QPushButton#secondary:disabled { color: #94a3b8; background: #f1f5f9; border-color: #e2e8f0; }

/* --- 日志区 (浅色终端风) --- */
QTextEdit#logView {
    background: #f8fafc; color: #334155;
    font-family: "Consolas", "Cascadia Code", "Courier New", monospace;
    font-size: 9.5pt; border: 1px solid #e2e8f0; border-radius: 8px;
    padding: 10px; line-height: 1.5;
}

/* --- Tab --- */
QTabWidget::pane { border: 1px solid #e2e8f0; border-radius: 8px; top: -1px; background: #ffffff; }
QTabBar::tab {
    padding: 8px 24px; font-size: 10pt; font-weight: 600; color: #64748b;
    background: #f1f5f9; border: 1px solid #e2e8f0; border-bottom: none;
    border-top-left-radius: 6px; border-top-right-radius: 6px; margin-right: 4px;
}
QTabBar::tab:selected { background: #ffffff; color: #2563eb; border-bottom: 2px solid #2563eb; }
QTabBar::tab:hover:!selected { background: #e2e8f0; color: #334155; }

/* --- 表格 --- */
QTableWidget {
    gridline-color: #f1f5f9; font-size: 10pt; color: #334155;
    border: 1px solid #e2e8f0; border-radius: 6px; background: #ffffff;
    alternate-background-color: #f8fafc;
}
QTableWidget::item { padding: 6px; }
QHeaderView::section {
    background: #1e293b; color: #f1f5f9; padding: 8px;
    font-weight: 600; font-size: 9.5pt; border: none;
}

/* --- 状态栏 --- */
QStatusBar { background: #1e293b; color: #e2e8f0; font-size: 9.5pt; }
QStatusBar QLabel { color: #e2e8f0; font-size: 9.5pt; padding: 2px 8px; }

/* --- 滚动条 --- */
QScrollBar:vertical { width: 10px; background: #f1f5f9; border-radius: 5px; }
QScrollBar::handle:vertical { background: #cbd5e1; border-radius: 5px; min-height: 30px; }
QScrollBar::handle:vertical:hover { background: #94a3b8; }
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
QScrollBar:horizontal { height: 10px; background: #f1f5f9; border-radius: 5px; }
QScrollBar::handle:horizontal { background: #cbd5e1; border-radius: 5px; min-width: 30px; }
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; }

/* --- 复选框 --- */
QCheckBox { spacing: 8px; color: #334155; font-size: 10pt; }
QCheckBox::indicator { width: 18px; height: 18px; border: 2px solid #cbd5e1; border-radius: 4px; background: #ffffff; }
QCheckBox::indicator:checked { background: #2563eb; border-color: #2563eb; }
"""


class SolverWorker(QObject):
    """后台仿真线程, 通过信号与 GUI 通信。"""
    log = pyqtSignal(str)
    result_single = pyqtSignal(dict)
    result_sweep = pyqtSignal(dict)
    finished = pyqtSignal()
    error = pyqtSignal(str)

    def __init__(self, mode, params):
        super().__init__()
        self.mode = mode
        self.params = params

    def run(self):
        try:
            from solver import Solver
            p = self.params
            solver = Solver(
                mot_path=p["mot_path"], out_dir=p["out_dir"],
                rms_current=p["rms_current"], speed_rpm=p["speed_rpm"],
                airgap_mm=p["airgap_mm"], magnet_temp_c=p["magnet_temp_c"],
                sec_radii_mm=p.get("sec_radii_mm", [28.25, 34.75]),
                motorcad_exe=p.get("motorcad_exe") or None,
                keep_open=p.get("keep_open", True),
                log_cb=lambda msg: self.log.emit(msg),
            )
            if self.mode == "single":
                results = solver.run_single()
                self.result_single.emit(results)
            else:
                results = solver.run_sweep(p["sweep_gaps"])
                self.result_sweep.emit(results)
        except Exception as e:
            tb = traceback.format_exc()
            self.error.emit("%s\n%s" % (e, tb))
        finally:
            self.finished.emit()


class ResultCard(QFrame):
    """结果数值卡片: 大数字 + 标签 + 单位"""
    def __init__(self, label, unit="N"):
        super().__init__()
        self.setFrameShape(QFrame.StyledPanel)
        self.setStyleSheet("""
            ResultCard { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 10px; }
        """)
        lay = QVBoxLayout(self)
        lay.setContentsMargins(16, 14, 16, 14)
        lay.setSpacing(2)

        self.lbl_label = QLabel(label)
        self.lbl_label.setObjectName("cardLabel")
        self.lbl_value = QLabel("—")
        self.lbl_value.setObjectName("cardValue")
        self.lbl_value.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        self.lbl_unit = QLabel(unit)
        self.lbl_unit.setObjectName("cardUnit")

        val_row = QHBoxLayout()
        val_row.setSpacing(4)
        val_row.addWidget(self.lbl_value)
        val_row.addWidget(self.lbl_unit)
        val_row.addStretch()

        lay.addWidget(self.lbl_label)
        lay.addLayout(val_row)

    def set_value(self, val, fmt="%.1f"):
        self.lbl_value.setText(fmt % val if val is not None else "—")

    def set_color(self, color):
        self.lbl_value.setStyleSheet("color: %s;" % color)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("MARS 轴向磁拉力仿真工具")
        self.resize(1200, 780)
        self.setMinimumSize(1024, 680)
        self.worker = None
        self.thread = None
        self.last_results = None
        self.last_mode = None
        self._build_ui()
        self._set_status("就绪", "#94a3b8")

    def _build_ui(self):
        central = QWidget()
        self.setCentralWidget(central)
        root = QHBoxLayout(central)
        root.setContentsMargins(12, 12, 12, 8)
        root.setSpacing(12)

        # ============ 左侧参数面板 ============
        left = QVBoxLayout()
        left.setSpacing(10)

        # 模型与路径
        gb_file = QGroupBox("模型与路径")
        gl = QGridLayout(gb_file)
        gl.setSpacing(8)
        gl.setVerticalSpacing(10)
        self.ed_mot = QLineEdit(DEFAULT_MOT)
        btn_mot = QPushButton("浏览")
        btn_mot.setObjectName("secondary")
        btn_mot.setFixedWidth(64)
        btn_mot.clicked.connect(self._browse_mot)
        gl.addWidget(QLabel("模型文件"), 0, 0)
        gl.addWidget(self.ed_mot, 0, 1)
        gl.addWidget(btn_mot, 0, 2)

        self.ed_out = QLineEdit(DEFAULT_OUT)
        btn_out = QPushButton("浏览")
        btn_out.setObjectName("secondary")
        btn_out.setFixedWidth(64)
        btn_out.clicked.connect(self._browse_out)
        gl.addWidget(QLabel("输出目录"), 1, 0)
        gl.addWidget(self.ed_out, 1, 1)
        gl.addWidget(btn_out, 1, 2)

        self.ed_mc = QLineEdit(DEFAULT_MC_EXE)
        gl.addWidget(QLabel("MC exe 路径"), 2, 0)
        gl.addWidget(self.ed_mc, 2, 1, 1, 2)
        left.addWidget(gb_file)

        # 工况参数
        gb_param = QGroupBox("工况参数")
        gp = QGridLayout(gb_param)
        gp.setSpacing(8)
        gp.setVerticalSpacing(10)
        self.sp_rms = QDoubleSpinBox(); self.sp_rms.setRange(0, 200); self.sp_rms.setValue(21.0); self.sp_rms.setSuffix(" A")
        self.sp_speed = QSpinBox(); self.sp_speed.setRange(100, 100000); self.sp_speed.setValue(5000); self.sp_speed.setSuffix(" rpm")
        self.sp_airgap = QDoubleSpinBox(); self.sp_airgap.setRange(0.1, 10); self.sp_airgap.setSingleStep(0.1); self.sp_airgap.setValue(1.0); self.sp_airgap.setSuffix(" mm")
        self.sp_temp = QDoubleSpinBox(); self.sp_temp.setRange(-40, 200); self.sp_temp.setValue(100.0); self.sp_temp.setSuffix(" °C")
        gp.addWidget(QLabel("RMS 电流"), 0, 0); gp.addWidget(self.sp_rms, 0, 1)
        gp.addWidget(QLabel("转速"), 0, 2); gp.addWidget(self.sp_speed, 0, 3)
        gp.addWidget(QLabel("气隙"), 1, 0); gp.addWidget(self.sp_airgap, 1, 1)
        gp.addWidget(QLabel("磁钢温度"), 1, 2); gp.addWidget(self.sp_temp, 1, 3)
        self.cb_keep = QCheckBox("运行后保持 Motor-CAD 打开")
        self.cb_keep.setChecked(True)
        gp.addWidget(self.cb_keep, 2, 0, 1, 4)
        left.addWidget(gb_param)

        # 运行模式 Tab
        self.tabs = QTabWidget()
        # 单点
        tab_single = QWidget()
        ts = QVBoxLayout(tab_single)
        ts.setContentsMargins(8, 12, 8, 8)
        self.btn_run_single = QPushButton("▶  运行单点仿真")
        self.btn_run_single.setMinimumHeight(42)
        self.btn_run_single.setStyleSheet("font-size: 11pt;")
        self.btn_run_single.clicked.connect(lambda: self._start_run("single"))
        ts.addWidget(self.btn_run_single)
        ts.addStretch()
        self.tabs.addTab(tab_single, "  单点仿真  ")

        # 气隙扫描
        tab_sweep = QWidget()
        sw = QGridLayout(tab_sweep)
        sw.setContentsMargins(8, 12, 8, 8)
        sw.setSpacing(8)
        self.sp_gap_start = QDoubleSpinBox(); self.sp_gap_start.setRange(0.1, 10); self.sp_gap_start.setSingleStep(0.1); self.sp_gap_start.setValue(0.6); self.sp_gap_start.setSuffix(" mm")
        self.sp_gap_end = QDoubleSpinBox(); self.sp_gap_end.setRange(0.1, 10); self.sp_gap_end.setSingleStep(0.1); self.sp_gap_end.setValue(1.5); self.sp_gap_end.setSuffix(" mm")
        self.sp_gap_step = QDoubleSpinBox(); self.sp_gap_step.setRange(0.05, 5); self.sp_gap_step.setSingleStep(0.05); self.sp_gap_step.setValue(0.2); self.sp_gap_step.setSuffix(" mm")
        sw.addWidget(QLabel("起始气隙"), 0, 0); sw.addWidget(self.sp_gap_start, 0, 1)
        sw.addWidget(QLabel("结束气隙"), 0, 2); sw.addWidget(self.sp_gap_end, 0, 3)
        sw.addWidget(QLabel("步长"), 1, 0); sw.addWidget(self.sp_gap_step, 1, 1)
        self.lbl_sweep_preview = QLabel("")
        self.lbl_sweep_preview.setStyleSheet("color: #64748b; font-size: 9pt; padding: 4px 0;")
        self.lbl_sweep_preview.setWordWrap(True)
        sw.addWidget(self.lbl_sweep_preview, 1, 2, 1, 2)
        self.btn_run_sweep = QPushButton("▶  运行气隙扫描")
        self.btn_run_sweep.setMinimumHeight(42)
        self.btn_run_sweep.setStyleSheet("font-size: 11pt;")
        self.btn_run_sweep.clicked.connect(lambda: self._start_run("sweep"))
        sw.addWidget(self.btn_run_sweep, 2, 0, 1, 4)
        sw.setRowStretch(3, 1)
        for sp in (self.sp_gap_start, self.sp_gap_end, self.sp_gap_step):
            sp.valueChanged.connect(self._update_sweep_preview)
        self._update_sweep_preview()
        self.tabs.addTab(tab_sweep, "  气隙扫描  ")
        left.addWidget(self.tabs)
        left.addStretch()

        left_w = QWidget()
        left_w.setLayout(left)
        left_w.setFixedWidth(400)

        # ============ 右侧: 结果 + 日志 ============
        right = QVBoxLayout()
        right.setSpacing(10)

        # --- 结果面板 (卡片式) ---
        gb_result = QGroupBox("运行结果")
        res_lay = QVBoxLayout(gb_result)
        res_lay.setSpacing(10)

        # 卡片行
        card_row = QHBoxLayout()
        card_row.setSpacing(10)
        self.card_oc = ResultCard("空载净轴向力", "N")
        self.card_ol = ResultCard("负载净轴向力", "N")
        self.card_ripple = ResultCard("纹波峰峰", "N")
        self.card_kneg = ResultCard("磁负刚度", "N/mm")
        self.card_kneg.setVisible(False)
        for c in (self.card_oc, self.card_ol, self.card_ripple, self.card_kneg):
            c.setMinimumHeight(88)
            card_row.addWidget(c)
        res_lay.addLayout(card_row)

        # 判据 + 摘要
        self.lbl_checks = QLabel("尚未运行仿真。")
        self.lbl_checks.setWordWrap(True)
        self.lbl_checks.setStyleSheet("""
            QLabel { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;
                     padding: 10px 14px; color: #64748b; font-size: 9.5pt; }
        """)
        res_lay.addWidget(self.lbl_checks)

        # 结果表格
        self.table_result = QTableWidget(0, 4)
        self.table_result.setHorizontalHeaderLabels(["工况", "净轴向力 (N)", "纹波峰峰 (N)", "备注"])
        self.table_result.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.table_result.verticalHeader().setVisible(False)
        self.table_result.setAlternatingRowColors(True)
        self.table_result.setVisible(False)
        self.table_result.setMaximumHeight(160)
        res_lay.addWidget(self.table_result)

        # 导出按钮
        btn_row = QHBoxLayout()
        self.btn_export_json = QPushButton("导出 JSON")
        self.btn_export_json.setObjectName("secondary")
        self.btn_export_json.setEnabled(False)
        self.btn_export_json.clicked.connect(self._export_json)
        self.btn_export_csv = QPushButton("导出 CSV")
        self.btn_export_csv.setObjectName("secondary")
        self.btn_export_csv.setEnabled(False)
        self.btn_export_csv.clicked.connect(self._export_csv)
        btn_row.addWidget(self.btn_export_json)
        btn_row.addWidget(self.btn_export_csv)
        btn_row.addStretch()
        res_lay.addLayout(btn_row)
        right.addWidget(gb_result, 4)

        # --- 日志区 ---
        gb_log = QGroupBox("运行日志")
        log_lay = QVBoxLayout(gb_log)
        log_lay.setContentsMargins(10, 14, 10, 10)
        self.txt_log = QTextEdit()
        self.txt_log.setObjectName("logView")
        self.txt_log.setReadOnly(True)
        log_lay.addWidget(self.txt_log)
        right.addWidget(gb_log, 5)

        right_w = QWidget()
        right_w.setLayout(right)

        splitter = QSplitter(Qt.Horizontal)
        splitter.addWidget(left_w)
        splitter.addWidget(right_w)
        splitter.setStretchFactor(0, 0)
        splitter.setStretchFactor(1, 1)
        splitter.setHandleWidth(2)
        root.addWidget(splitter)

        # 状态栏
        self.statusBar = QStatusBar()
        self.setStatusBar(self.statusBar)
        self.lbl_status = QLabel("就绪")
        self.lbl_runtime = QLabel("")
        self.statusBar.addWidget(self.lbl_status, 1)
        self.statusBar.addPermanentWidget(self.lbl_runtime)

    # ================================================================
    # 交互
    # ================================================================
    def _set_status(self, text, color="#e2e8f0"):
        self.lbl_status.setText(text)
        self.lbl_status.setStyleSheet("color: %s;" % color)

    def _browse_mot(self):
        path, _ = QFileDialog.getOpenFileName(self, "选择 Motor-CAD 模型", self.ed_mot.text(), "Motor-CAD (*.mot)")
        if path:
            self.ed_mot.setText(path)

    def _browse_out(self):
        path = QFileDialog.getExistingDirectory(self, "选择输出目录", self.ed_out.text())
        if path:
            self.ed_out.setText(path)

    def _update_sweep_preview(self):
        gaps = self._calc_sweep_gaps()
        if gaps:
            est = len(gaps) * 2.5
            self.lbl_sweep_preview.setText("将求解 %d 个气隙点 (%.1f~%.1fmm), 预计约 %.0f 分钟"
                                            % (len(gaps), gaps[0], gaps[-1], est))
        else:
            self.lbl_sweep_preview.setText("参数无效 (起始应 < 结束, 步长 > 0)")

    def _calc_sweep_gaps(self):
        s, e, step = self.sp_gap_start.value(), self.sp_gap_end.value(), self.sp_gap_step.value()
        if s >= e or step <= 0:
            return []
        gaps = []
        g = s
        while g <= e + 1e-9:
            gaps.append(round(g, 4))
            g += step
        return gaps

    def _collect_params(self):
        return {
            "mot_path": self.ed_mot.text().strip(),
            "out_dir": self.ed_out.text().strip(),
            "rms_current": self.sp_rms.value(),
            "speed_rpm": self.sp_speed.value(),
            "airgap_mm": self.sp_airgap.value(),
            "magnet_temp_c": self.sp_temp.value(),
            "motorcad_exe": self.ed_mc.text().strip(),
            "keep_open": self.cb_keep.isChecked(),
            "sec_radii_mm": [28.25, 34.75],
        }

    def _start_run(self, mode):
        if self.worker and self.thread and self.thread.isRunning():
            QMessageBox.warning(self, "正在运行", "仿真正在进行中, 请等待完成。")
            return
        p = self._collect_params()
        if not os.path.isfile(p["mot_path"]):
            QMessageBox.critical(self, "错误", "模型文件不存在:\n%s" % p["mot_path"])
            return
        if mode == "sweep":
            gaps = self._calc_sweep_gaps()
            if not gaps:
                QMessageBox.critical(self, "错误", "气隙扫描参数无效。")
                return
            p["sweep_gaps"] = gaps
            if len(gaps) > 10:
                reply = QMessageBox.question(self, "确认",
                    "将求解 %d 个气隙点, 预计约 %d 分钟, 确认?" % (len(gaps), len(gaps) * 3))
                if reply != QMessageBox.Yes:
                    return

        # 重置界面
        self.last_results = None
        self.txt_log.clear()
        self.table_result.setVisible(False)
        self.card_oc.set_value(None); self.card_oc.set_color("#1e293b")
        self.card_ol.set_value(None); self.card_ol.set_color("#1e293b")
        self.card_ripple.set_value(None); self.card_ripple.set_color("#1e293b")
        self.card_kneg.setVisible(False)
        self.lbl_checks.setText("仿真运行中...")
        self.lbl_checks.setStyleSheet("""
            QLabel { background: #fffbeb; border: 1px solid #fbbf24; border-radius: 6px;
                     padding: 10px 14px; color: #92400e; font-size: 9.5pt; }
        """)
        self.btn_export_json.setEnabled(False)
        self.btn_export_csv.setEnabled(False)
        self._set_running(True)
        self._set_status("● 运行中... Motor-CAD 求解中", "#fbbf24")
        self.lbl_runtime.setText("")

        # 启动线程
        self.thread = QThread()
        self.worker = SolverWorker(mode, p)
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.worker.log.connect(self._append_log)
        self.worker.result_single.connect(self._on_single_result)
        self.worker.result_sweep.connect(self._on_sweep_result)
        self.worker.error.connect(self._on_error)
        self.worker.finished.connect(self._on_finished)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.last_mode = mode
        self._run_start_time = __import__("time").time()
        self.thread.start()

    def _set_running(self, running):
        for btn in (self.btn_run_single, self.btn_run_sweep):
            btn.setEnabled(not running)
        if running:
            self.btn_run_single.setText("⏳  运行中...")
            self.btn_run_sweep.setText("⏳  运行中...")
        else:
            self.btn_run_single.setText("▶  运行单点仿真")
            self.btn_run_sweep.setText("▶  运行气隙扫描")

    def _append_log(self, msg):
        """彩色日志: 根据内容前缀着色"""
        msg = msg.rstrip()
        if not msg:
            return
        # 判断颜色
        if msg.startswith("[结论]") or "✓" in msg:
            color = "#16a34a"  # 绿
            weight = "600"
        elif msg.startswith("[错误]") or msg.startswith("[ERROR]") or "Traceback" in msg:
            color = "#dc2626"  # 红
            weight = "600"
        elif msg.startswith("[警告]"):
            color = "#d97706"  # 橙
            weight = "600"
        elif msg.startswith("[提示]"):
            color = "#2563eb"  # 蓝
            weight = "500"
        elif msg.startswith("== ") or msg.startswith("启动") or "求解" in msg[:6]:
            color = "#475569"  # 深灰 (阶段标题)
            weight = "600"
        elif "RESULTS:" in msg or "结果文件:" in msg or "扫描结果:" in msg:
            color = "#7c3aed"  # 紫 (文件路径)
            weight = "500"
        else:
            color = "#334155"  # 默认
            weight = "400"

        import html
        escaped = html.escape(msg)
        self.txt_log.append('<span style="color:%s;font-weight:%s;">%s</span>' % (color, weight, escaped))
        sb = self.txt_log.verticalScrollBar()
        sb.setValue(sb.maximum())

    def _on_single_result(self, results):
        try:
            self.last_results = results
            forces = results.get("axial_forces", {}) or {}
            checks = results.get("checks", {}) or {}

            # 安全访问: forces[graph] 可能为 None
            oc_data = forces.get("Fr_Rotor_OC_Lumped") or {}
            ol_data = forces.get("Fr_Rotor_OL_Lumped") or {}
            oc = oc_data.get("stats", {}) or {}
            ol = ol_data.get("stats", {}) or {}
            self.card_oc.set_value(oc.get("mean"))
            self.card_ol.set_value(ol.get("mean"))
            self.card_ripple.set_value(oc.get("pk2pk"))
            self.card_oc.set_color("#16a34a")
            self.card_ol.set_color("#2563eb")
            self.card_ripple.set_color("#64748b")

            # 判据文本
            parts = []
            ar_ol = (checks.get("action_reaction_OL") or {}).get("imbalance_pct")
            ar_oc = (checks.get("action_reaction_OC") or {}).get("imbalance_pct")
            if ar_ol is not None:
                ok = ar_ol < 5
                parts.append(("%s 作用反作用 OL=%.1f%%" % ("✓" if ok else "✗", ar_ol),
                              "#16a34a" if ok else "#dc2626"))
            if ar_oc is not None:
                ok = ar_oc < 5
                parts.append(("%s OC=%.1f%%" % ("✓" if ok else "✗", ar_oc),
                              "#16a34a" if ok else "#dc2626"))
            tq = checks.get("torque_crosscheck") or {}
            if tq.get("sum_Ft_x_r_Nm_t0") and tq.get("torque_graph_mean_Nm"):
                diff = abs(tq["sum_Ft_x_r_Nm_t0"] - tq["torque_graph_mean_Nm"]) \
                    / max(abs(tq["torque_graph_mean_Nm"]), 1e-9) * 100
                ok = diff < 10
                parts.append(("%s 转矩交叉=%.1f%% (%.3f vs %.3f Nm)"
                              % ("✓" if ok else "✗", diff,
                                 tq["sum_Ft_x_r_Nm_t0"], tq["torque_graph_mean_Nm"]),
                              "#16a34a" if ok else "#dc2626"))
            an = checks.get("analytic") or {}
            if an.get("F_est_N"):
                parts.append(("解析量级=%.0fN (同量级)" % an["F_est_N"], "#7c3aed"))

            html_parts = " &nbsp;&nbsp; ".join(
                '<span style="color:%s;font-weight:600;">%s</span>' % (c, t) for t, c in parts
            )
            self.lbl_checks.setText(html_parts or "无判据数据")
            self.lbl_checks.setStyleSheet("""
                QLabel { background: #f0fdf4; border: 1px solid #86efac; border-radius: 6px;
                         padding: 10px 14px; font-size: 9.5pt; }
            """)

            # 表格
            rows = []
            rms = (results.get("params") or {}).get("rms_current", 0)
            for case, label in (("OC", "空载 (开路)"), ("OL", "负载 (RMS %.0fA)" % rms)):
                v = forces.get("Fr_Rotor_%s_Lumped" % case)
                if v and v.get("stats"):
                    s = v["stats"]
                    rows.append((label, "%.1f" % s["mean"], "%.2f" % s["pk2pk"], "指向定子"))
            self._fill_table(rows)
            self.btn_export_json.setEnabled(True)
            self.btn_export_csv.setEnabled(True)
        except Exception as e:
            self._append_log("[错误] 结果显示异常: %s" % e)
            traceback.print_exc()

    def _on_sweep_result(self, results):
        try:
            self.last_results = results
            cases = results.get("cases", []) or []
            kneg = results.get("kneg_N_per_mm", {}) or {}

            # 卡片: 用第一个和最后一个气隙的力
            if cases:
                first = cases[0] or {}
                last = cases[-1] or {}
                foc_first = (first.get("F_OC") or {}).get("mean")
                foc_last = (last.get("F_OC") or {}).get("mean")
                self.card_oc.set_value(foc_first)
                self.card_oc.lbl_label.setText(
                    "起始气隙 %.2fmm 空载力" % (first.get("airgap_mm", 0) or 0))
                self.card_ol.set_value(foc_last)
                self.card_ol.lbl_label.setText(
                    "结束气隙 %.2fmm 空载力" % (last.get("airgap_mm", 0) or 0))
                # 平均纹波
                ripples = [(c.get("F_OC") or {}).get("pk2pk", 0) for c in cases
                           if (c.get("F_OC") or {}).get("pk2pk") is not None]
                if ripples:
                    self.card_ripple.set_value(sum(ripples) / len(ripples))
                self.card_oc.set_color("#16a34a")
                self.card_ol.set_color("#2563eb")
                self.card_ripple.set_color("#64748b")
                # 磁负刚度卡片
                if kneg:
                    central = kneg.get("central")
                    if central is not None:
                        self.card_kneg.set_value(central)
                        self.card_kneg.set_color("#7c3aed")
                        self.card_kneg.setVisible(True)

            kneg_txt = " | ".join("%s=%.0f" % (k, v) for k, v in kneg.items()) if kneg else "—"
            self.lbl_checks.setText(
                '<span style="color:#16a34a;font-weight:600;">✓ 扫描完成</span> &nbsp;&nbsp; '
                '<span style="color:#7c3aed;">磁负刚度 (N/mm): %s</span>' % kneg_txt)
            self.lbl_checks.setStyleSheet("""
                QLabel { background: #f0fdf4; border: 1px solid #86efac; border-radius: 6px;
                         padding: 10px 14px; font-size: 9.5pt; }
            """)

            rows = []
            for c in cases:
                if not c:
                    continue
                g = c.get("airgap_mm", 0)
                foc = (c.get("F_OC") or {}).get("mean", 0)
                fol = (c.get("F_OL") or {}).get("mean", 0)
                rip = (c.get("F_OC") or {}).get("pk2pk", 0)
                rows.append(("气隙 %.2fmm" % g, "OC=%.1f / OL=%.1f" % (foc, fol),
                             "%.2f" % rip, "耗时%.0fs" % (c.get("solve_seconds", 0) or 0)))
            self._fill_table(rows)
            self.btn_export_json.setEnabled(True)
            self.btn_export_csv.setEnabled(False)
        except Exception as e:
            self._append_log("[错误] 扫描结果显示异常: %s" % e)
            traceback.print_exc()

    def _fill_table(self, rows):
        self.table_result.setVisible(True)
        self.table_result.setRowCount(len(rows))
        for i, (a, b, c, d) in enumerate(rows):
            for j, val in enumerate((a, b, c, d)):
                item = QTableWidgetItem(str(val))
                item.setTextAlignment(Qt.AlignCenter)
                self.table_result.setItem(i, j, item)

    def _on_error(self, msg):
        try:
            self.lbl_checks.setText('<span style="color:#dc2626;font-weight:600;">✗ 运行出错, 详见日志</span>')
            self.lbl_checks.setStyleSheet("""
                QLabel { background: #fef2f2; border: 1px solid #fca5a5; border-radius: 6px;
                         padding: 10px 14px; font-size: 9.5pt; }
            """)
            self._append_log("[错误] %s" % msg)
        except Exception:
            traceback.print_exc()

    def _on_finished(self):
        try:
            elapsed = time.time() - getattr(self, "_run_start_time", time.time())
            self._set_running(False)
            # 检查是否有结果 (成功) 还是出错
            if self.last_results is not None:
                self._set_status("✓ 运行完成 — 结果已显示在上方", "#86efac")
                self.lbl_runtime.setText("总耗时 %.0fs" % elapsed)
            else:
                self._set_status("✗ 运行出错 — 详见日志", "#fca5a5")
                self.lbl_runtime.setText("")
        except Exception as e:
            traceback.print_exc()
        finally:
            # 延迟清理线程引用, 避免与 deleteLater 竞态
            from PyQt5.QtCore import QTimer
            QTimer.singleShot(100, self._cleanup_thread)

    def _cleanup_thread(self):
        self.worker = None
        self.thread = None

    def _export_json(self):
        if not self.last_results:
            return
        path, _ = QFileDialog.getSaveFileName(self, "导出 JSON", "axialforce_result.json", "JSON (*.json)")
        if path:
            with open(path, "w", encoding="utf-8") as f:
                json.dump(self.last_results, f, ensure_ascii=False, indent=2)
            self._append_log("[提示] JSON 已导出 -> %s" % path)

    def _export_csv(self):
        if not self.last_results or self.last_mode != "single":
            return
        path, _ = QFileDialog.getSaveFileName(self, "导出 CSV", "axial_force.csv", "CSV (*.csv)")
        if path:
            forces = self.last_results.get("axial_forces", {})
            graphs = ["Fr_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped",
                      "Fr_Rotor_OC_Lumped", "Fr_Stator_OC_Lumped"]
            nmax = max(len(v["series_N"]) if v else 0 for v in forces.values())
            with open(path, "w", encoding="utf-8") as f:
                f.write("tstep," + ",".join(graphs) + "\n")
                for i in range(nmax):
                    row = [str(i)]
                    for g in graphs:
                        v = forces.get(g)
                        row.append("%.4f" % v["series_N"][i] if v and i < len(v["series_N"]) else "")
                    f.write(",".join(row) + "\n")
            self._append_log("[提示] CSV 已导出 -> %s" % path)


def main():
    app = QApplication(sys.argv)
    app.setStyle("Fusion")
    app.setStyleSheet(STYLE)
    # 关键: 防止子窗口/对话框关闭时触发整个应用退出
    app.setQuitOnLastWindowClosed(False)
    # 全局默认字体
    font = QFont("Microsoft YaHei", 10)
    app.setFont(font)

    # 全局异常钩子: 捕获未处理异常, 防止 --windowed 模式下静默闪退
    def _excepthook(exc_type, exc_value, exc_tb):
        import traceback
        msg = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
        try:
            with open(os.path.join(os.path.expanduser("~"), "axial_force_gui_error.log"),
                      "a", encoding="utf-8") as f:
                f.write("\n=== %s ===\n%s" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg))
        except Exception:
            pass
        # 尝试在 GUI 中显示错误
        try:
            QMessageBox.critical(None, "程序异常",
                                 "发生未捕获异常:\n%s\n\n详情已写入 ~/axial_force_gui_error.log"
                                 % str(exc_value))
        except Exception:
            pass
    sys.excepthook = _excepthook

    w = MainWindow()
    w.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
