"""PySide6 GUI for PCB axial flux motor automated simulation (Phase 1).

Features:
- Simulation plan editor (model path, scan variables, cases)
- Scan execution with real-time progress and results
- Colored log view
- Results table with live updates
- Save/load simulation_plan.json
- 5-layer crash protection
- Industrial software style

All source is ASCII.
"""

from __future__ import annotations

import json
import os
import sys
import traceback
from datetime import datetime
from pathlib import Path

from PySide6.QtCore import Qt, QThread, Signal, QTimer
from PySide6.QtGui import QFont, QTextCursor, QAction
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QGroupBox, QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
    QHeaderView, QTextEdit, QStatusBar, QFileDialog, QMessageBox,
    QSplitter, QAbstractItemView, QComboBox, QDoubleSpinBox, QSpinBox,
    QProgressBar, QTabWidget, QPlainTextEdit,
)

# Add project root to path
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

from src.solver_core import MotorCADSolver, METRIC_LABELS, METRIC_KEYS  # noqa: E402
from src.scan_engine import ScanEngine, estimate_total_time  # noqa: E402
from src.plan_schema import SimulationPlan, ScanVariable  # noqa: E402
from src.experience_db import ExperienceDB  # noqa: E402


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

DEFAULT_MODEL = str(PROJECT_ROOT / "models" / "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")
DEFAULT_OUTPUT = str(PROJECT_ROOT / "output")
DEFAULT_EXPERIENCE_DB = str(PROJECT_ROOT / "experience" / "experience.db")

# Phase 1 core metrics (always shown first)
CORE_METRICS = ["tavg_nm", "ripple_pct", "efficiency_pct", "total_losses_w"]
# Additional metrics (shown after core)
EXTRA_METRICS = [k for k in METRIC_KEYS if k not in CORE_METRICS]

# Variable table columns
VAR_COLUMNS = ["Variable Name", "Display Name", "Unit", "Start", "Stop", "Step", "Values (comma-sep)"]


# ---------------------------------------------------------------------------
# Scan Worker (QThread)
# ---------------------------------------------------------------------------

class ScanWorker(QThread):
    """Runs a parameter scan in a background thread."""
    log = Signal(str)
    progress = Signal(int, int)  # current, total
    result_row = Signal(dict)
    finished_ok = Signal(dict)  # summary
    failed = Signal(str)

    def __init__(self, plan: SimulationPlan, output_dir: str):
        super().__init__()
        self.plan = plan
        self.output_dir = output_dir
        self._cancel = False
        self._engine: ScanEngine | None = None

    def cancel(self) -> None:
        self._cancel = True
        if self._engine:
            self._engine.cancel()

    def run(self) -> None:
        try:
            solver = MotorCADSolver(log_cb=lambda m: self.log.emit(m))
            solver.connect()

            points, var_names = self.plan.generate_points()
            self.log.emit(f"Generated {len(points)} scan points")

            self._engine = ScanEngine(
                solver=solver,
                model_path=self.plan.model_path,
                output_dir=self.output_dir,
                scan_name=self.plan.plan_id,
                log_cb=lambda m: self.log.emit(m),
                progress_cb=lambda c, t: self.progress.emit(c, t),
                row_cb=lambda r: self.result_row.emit(r),
            )

            result = self._engine.run(points, var_names=var_names)
            solver.disconnect()
            self.finished_ok.emit(result)
        except Exception as exc:
            self.failed.emit(f"{type(exc).__name__}: {exc}\n{traceback.format_exc()}")


# ---------------------------------------------------------------------------
# Variable Table Widget
# ---------------------------------------------------------------------------

class VariableTableWidget(QTableWidget):
    """Table for editing scan variables."""

    def __init__(self, parent=None):
        super().__init__(0, len(VAR_COLUMNS), parent)
        self.setHorizontalHeaderLabels(VAR_COLUMNS)
        self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setAlternatingRowColors(True)
        self.verticalHeader().setVisible(False)

    def add_row(self, var: ScanVariable | None = None) -> None:
        row = self.rowCount()
        self.insertRow(row)
        if var:
            vals_str = ", ".join(str(v) for v in var.values) if var.values else ""
            items = [
                var.name, var.display_name, var.unit,
                str(var.start) if var.start else "",
                str(var.stop) if var.stop else "",
                str(var.step) if var.step else "",
                vals_str,
            ]
        else:
            items = ["", "", "", "", "", "", ""]
        for col, text in enumerate(items):
            self.setItem(row, col, QTableWidgetItem(text))

    def remove_selected(self) -> None:
        rows = sorted(set(idx.row() for idx in self.selectedIndexes()), reverse=True)
        for row in rows:
            self.removeRow(row)

    def get_variables(self) -> list[ScanVariable]:
        """Parse table into ScanVariable list."""
        variables = []
        for row in range(self.rowCount()):
            name = self.item(row, 0).text().strip() if self.item(row, 0) else ""
            if not name:
                continue
            display = self.item(row, 1).text().strip() if self.item(row, 1) else name
            unit = self.item(row, 2).text().strip() if self.item(row, 2) else ""
            start = float(self.item(row, 3).text()) if self.item(row, 3) and self.item(row, 3).text() else 0.0
            stop = float(self.item(row, 4).text()) if self.item(row, 4) and self.item(row, 4).text() else 0.0
            step = float(self.item(row, 5).text()) if self.item(row, 5) and self.item(row, 5).text() else 0.0
            values_text = self.item(row, 6).text().strip() if self.item(row, 6) else ""
            values = [float(v.strip()) for v in values_text.split(",") if v.strip()] if values_text else []

            var = ScanVariable(
                name=name, display_name=display, unit=unit,
                start=start, stop=stop, step=step, values=values,
            )
            variables.append(var)
        return variables


# ---------------------------------------------------------------------------
# Results Table Widget
# ---------------------------------------------------------------------------

class ResultsTableWidget(QTableWidget):
    """Table for displaying scan results."""

    def __init__(self, parent=None):
        super().__init__(0, 0, parent)
        self._var_names: list[str] = []
        self._metric_cols: list[str] = []
        self.setAlternatingRowColors(True)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.verticalHeader().setVisible(False)

    def setup_columns(self, var_names: list[str]) -> None:
        self._var_names = var_names
        self._metric_cols = CORE_METRICS + EXTRA_METRICS
        cols = ["#", "Status", "Time(s)"] + var_names + self._metric_cols
        self.setColumnCount(len(cols))
        self.setHorizontalHeaderLabels(cols)
        self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.setRowCount(0)

    def add_result_row(self, row: dict) -> None:
        r = self.rowCount()
        self.insertRow(r)
        col = 0
        # Index
        self.setItem(r, col, QTableWidgetItem(str(row.get("run_index", ""))))
        col += 1
        # Status
        status = str(row.get("status", ""))
        item = QTableWidgetItem(status)
        if status == "OK":
            item.setForeground(Qt.darkGreen)
        elif status == "FAILED":
            item.setForeground(Qt.red)
        self.setItem(r, col, item)
        col += 1
        # Time
        self.setItem(r, col, QTableWidgetItem(str(row.get("seconds", ""))))
        col += 1
        # Variables
        for vname in self._var_names:
            self.setItem(r, col, QTableWidgetItem(str(row.get(vname, ""))))
            col += 1
        # Metrics
        for mkey in self._metric_cols:
            val = row.get(mkey, "")
            if isinstance(val, float):
                self.setItem(r, col, QTableWidgetItem(f"{val:.4f}"))
            else:
                self.setItem(r, col, QTableWidgetItem(str(val)))
            col += 1


# ---------------------------------------------------------------------------
# Log Widget
# ---------------------------------------------------------------------------

class LogWidget(QTextEdit):
    """Colored log display widget."""

    COLORS = {
        "INFO": "#334155",
        "OK": "#16a34a",
        "SUCCESS": "#16a34a",
        "WARNING": "#d97706",
        "WARN": "#d97706",
        "ERROR": "#dc2626",
        "FAIL": "#dc2626",
        "FATAL": "#dc2626",
        "DEBUG": "#64748b",
    }

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setReadOnly(True)
        self.setStyleSheet("""
            QTextEdit {
                background: #f8fafc;
                color: #334155;
                border: 1px solid #e2e8f0;
                border-radius: 8px;
                padding: 10px;
                font-family: Consolas, Cascadia Code, monospace;
                font-size: 9.5pt;
            }
        """)

    def append_log(self, text: str) -> None:
        # Determine color based on content
        color = "#334155"
        upper = text.upper()
        for keyword, c in self.COLORS.items():
            if keyword in upper:
                color = c
                break
        # Check for special patterns
        if "SCAN COMPLETE" in upper or "OK=" in upper:
            color = "#16a34a"
        elif "SKIP" in upper:
            color = "#7c3aed"

        self.setTextColor(Qt.GlobalColor(Qt.color0) if False else Qt.black)  # placeholder
        # Use HTML for colored output
        from PySide6.QtGui import QColor
        self.setTextColor(QColor(color))
        self.append(text)
        self.moveCursor(QTextCursor.End)
        self.ensureCursorVisible()


# ---------------------------------------------------------------------------
# Main Window
# ---------------------------------------------------------------------------

class MainWindow(QMainWindow):
    """Main application window."""

    def __init__(self):
        super().__init__()
        self.setWindowTitle("PCB Axial Flux Motor - Automated Simulation")
        self.resize(1400, 900)
        self.worker: ScanWorker | None = None
        self._plan = SimulationPlan(model_path=DEFAULT_MODEL)
        self._experience_db = ExperienceDB(DEFAULT_EXPERIENCE_DB)
        self._last_results: list[dict] = []

        self._build_ui()
        self._apply_style()
        self._update_status("Ready")

    def _build_ui(self) -> None:
        central = QWidget()
        self.setCentralWidget(central)
        main_layout = QVBoxLayout(central)
        main_layout.setContentsMargins(10, 10, 10, 10)
        main_layout.setSpacing(8)

        # Top: model path + plan actions
        top_group = QGroupBox("Simulation Plan")
        top_layout = QHBoxLayout(top_group)

        top_layout.addWidget(QLabel("Model:"))
        self.model_edit = QLineEdit(DEFAULT_MODEL)
        self.model_edit.setMinimumWidth(400)
        top_layout.addWidget(self.model_edit)

        browse_btn = QPushButton("Browse...")
        browse_btn.clicked.connect(self._browse_model)
        top_layout.addWidget(browse_btn)

        top_layout.addStretch()

        save_btn = QPushButton("Save Plan")
        save_btn.clicked.connect(self._save_plan)
        top_layout.addWidget(save_btn)

        load_btn = QPushButton("Load Plan")
        load_btn.clicked.connect(self._load_plan)
        top_layout.addWidget(load_btn)

        main_layout.addWidget(top_group)

        # Main splitter: left (plan editor) | right (results + log)
        splitter = QSplitter(Qt.Horizontal)

        # Left: variable editor
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)
        left_layout.setContentsMargins(0, 0, 0, 0)

        var_group = QGroupBox("Scan Variables")
        var_layout = QVBoxLayout(var_group)

        self.var_table = VariableTableWidget()
        var_layout.addWidget(self.var_table)

        btn_layout = QHBoxLayout()
        add_btn = QPushButton("Add Variable")
        add_btn.clicked.connect(lambda: self.var_table.add_row())
        btn_layout.addWidget(add_btn)

        del_btn = QPushButton("Remove Selected")
        del_btn.clicked.connect(self.var_table.remove_selected)
        btn_layout.addWidget(del_btn)

        btn_layout.addStretch()
        var_layout.addLayout(btn_layout)

        # Estimate label
        self.estimate_label = QLabel("Points: 0 | Est. time: 0 min")
        self.estimate_label.setStyleSheet("color: #64748b; font-size: 9pt;")
        var_layout.addWidget(self.estimate_label)

        left_layout.addWidget(var_group)

        # Run controls
        run_group = QGroupBox("Execution")
        run_layout = QVBoxLayout(run_group)

        run_btn_layout = QHBoxLayout()
        self.start_btn = QPushButton("Start Scan")
        self.start_btn.setMinimumHeight(40)
        self.start_btn.setStyleSheet("""
            QPushButton {
                background: #2563eb; color: white; border: none;
                border-radius: 6px; font-weight: 600; font-size: 11pt;
            }
            QPushButton:hover { background: #1d4ed8; }
            QPushButton:disabled { background: #94a3b8; }
        """)
        self.start_btn.clicked.connect(self._start_scan)
        run_btn_layout.addWidget(self.start_btn)

        self.stop_btn = QPushButton("Stop")
        self.stop_btn.setMinimumHeight(40)
        self.stop_btn.setEnabled(False)
        self.stop_btn.setStyleSheet("""
            QPushButton {
                background: #dc2626; color: white; border: none;
                border-radius: 6px; font-weight: 600; font-size: 11pt;
            }
            QPushButton:hover { background: #b91c1c; }
            QPushButton:disabled { background: #94a3b8; }
        """)
        self.stop_btn.clicked.connect(self._stop_scan)
        run_btn_layout.addWidget(self.stop_btn)

        self.recommend_btn = QPushButton("Recommend Next")
        self.recommend_btn.setMinimumHeight(40)
        self.recommend_btn.setEnabled(False)
        self.recommend_btn.setStyleSheet("""
            QPushButton {
                background: #7c3aed; color: white; border: none;
                border-radius: 6px; font-weight: 600; font-size: 11pt;
            }
            QPushButton:hover { background: #6d28d9; }
            QPushButton:disabled { background: #94a3b8; }
        """)
        self.recommend_btn.clicked.connect(self._show_recommendations)
        run_btn_layout.addWidget(self.recommend_btn)

        run_layout.addLayout(run_btn_layout)

        # Progress bar
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)
        self.progress_bar.setValue(0)
        self.progress_bar.setFormat("%p% (%v/%m)")
        run_layout.addWidget(self.progress_bar)

        left_layout.addWidget(run_group)
        left_layout.addStretch()

        splitter.addWidget(left_widget)

        # Right: results + log (vertical splitter)
        right_splitter = QSplitter(Qt.Vertical)

        # Results table
        results_group = QGroupBox("Results")
        results_layout = QVBoxLayout(results_group)
        self.results_table = ResultsTableWidget()
        results_layout.addWidget(self.results_table)
        right_splitter.addWidget(results_group)

        # Log
        log_group = QGroupBox("Log")
        log_layout = QVBoxLayout(log_group)
        self.log_widget = LogWidget()
        log_layout.addWidget(self.log_widget)
        right_splitter.addWidget(log_group)

        right_splitter.setSizes([400, 300])
        splitter.addWidget(right_splitter)
        splitter.setSizes([450, 900])

        main_layout.addWidget(splitter)

        # Status bar
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        self.status_label = QLabel("Ready")
        self.status_bar.addWidget(self.status_label, 1)

        # Add default airgap variable
        self.var_table.add_row(ScanVariable(
            name="Airgap", display_name="Airgap", unit="mm",
            values=[0.6, 1.0, 1.5],
        ))
        self._update_estimate()
        self.var_table.itemChanged.connect(lambda: self._update_estimate())

    def _apply_style(self) -> None:
        self.setStyleSheet("""
            QMainWindow { background: #eef1f5; }
            QGroupBox {
                background: #ffffff; border: 1px solid #e2e8f0;
                border-radius: 8px; margin-top: 16px; padding-top: 14px;
                font-weight: 600; font-size: 11pt;
            }
            QGroupBox::title {
                subcontrol-origin: margin; left: 14px; padding: 0 8px;
                background: #ffffff;
            }
            QLineEdit {
                padding: 6px 10px; border: 1px solid #cbd5e1;
                border-radius: 6px; background: #f8fafc;
            }
            QLineEdit:focus { border-color: #2563eb; background: #ffffff; }
            QPushButton {
                padding: 6px 16px; border-radius: 6px;
                background: #f1f5f9; border: 1px solid #cbd5e1;
                font-weight: 500;
            }
            QPushButton:hover { background: #e2e8f0; }
            QTableWidget {
                background: #ffffff; border: 1px solid #e2e8f0;
                border-radius: 6px; gridline-color: #f1f5f9;
            }
            QHeaderView::section {
                background: #f8fafc; padding: 6px; border: none;
                border-bottom: 2px solid #e2e8f0; font-weight: 600;
            }
            QProgressBar {
                border: 1px solid #e2e8f0; border-radius: 6px;
                text-align: center; background: #f8fafc; height: 24px;
            }
            QProgressBar::chunk { background: #2563eb; border-radius: 5px; }
            QStatusBar { background: #1e293b; color: #e2e8f0; }
        """)

    def _update_status(self, text: str) -> None:
        self.status_label.setText(text)

    def _update_estimate(self) -> None:
        variables = self.var_table.get_variables()
        total = 1
        for v in variables:
            vals = v.get_values()
            if vals:
                total *= len(vals)
        if not variables:
            total = 0
        est = estimate_total_time([{"index": i} for i in range(total)])
        self.estimate_label.setText(
            f"Points: {total} | Est. time: {est['total_min']} min ({est['total_h']} h)"
        )

    def _browse_model(self) -> None:
        path, _ = QFileDialog.getOpenFileName(
            self, "Select Motor-CAD Model", str(PROJECT_ROOT / "models"),
            "Motor-CAD Files (*.mot);;All Files (*)"
        )
        if path:
            self.model_edit.setText(path)

    def _build_plan(self) -> SimulationPlan:
        plan = SimulationPlan(model_path=self.model_edit.text().strip())
        plan.variables = self.var_table.get_variables()
        return plan

    def _save_plan(self) -> None:
        plan = self._build_plan()
        path, _ = QFileDialog.getSaveFileName(
            self, "Save Simulation Plan", str(PROJECT_ROOT / "simulation_plan.json"),
            "JSON Files (*.json);;All Files (*)"
        )
        if path:
            plan.save(path)
            self._log(f"Plan saved: {path}")

    def _load_plan(self) -> None:
        path, _ = QFileDialog.getOpenFileName(
            self, "Load Simulation Plan", str(PROJECT_ROOT),
            "JSON Files (*.json);;All Files (*)"
        )
        if path:
            try:
                plan = SimulationPlan.load(path)
                self.model_edit.setText(plan.model_path)
                self.var_table.setRowCount(0)
                for v in plan.variables:
                    self.var_table.add_row(v)
                self._update_estimate()
                self._log(f"Plan loaded: {path}")
            except Exception as exc:
                QMessageBox.critical(self, "Load Error", f"Failed to load plan:\n{exc}")

    def _log(self, text: str) -> None:
        self.log_widget.append_log(text)

    def _start_scan(self) -> None:
        plan = self._build_plan()
        ok, errors = plan.validate()
        if not ok:
            QMessageBox.warning(self, "Validation Error", "\n".join(errors))
            return

        # Git preflight
        try:
            import subprocess
            safe = f"safe.directory={PROJECT_ROOT.as_posix()}"
            dirty = subprocess.check_output(
                ["git", "-c", safe, "status", "--porcelain", "--untracked-files=no"],
                cwd=PROJECT_ROOT, text=True, stderr=subprocess.STDOUT,
            ).strip()
            if dirty:
                reply = QMessageBox.question(
                    self, "Git Not Clean",
                    "Working directory has uncommitted changes. Continue anyway?",
                    QMessageBox.Yes | QMessageBox.No,
                )
                if reply == QMessageBox.No:
                    return
        except Exception:
            pass  # Git not available, skip preflight

        # Setup results table
        points, var_names = plan.generate_points()
        self.results_table.setup_columns(var_names)
        self.progress_bar.setRange(0, len(points))
        self.progress_bar.setValue(0)
        self._last_results = []
        self.recommend_btn.setEnabled(False)

        # Start worker
        self.worker = ScanWorker(plan, DEFAULT_OUTPUT)
        self.worker.log.connect(self._log)
        self.worker.progress.connect(self._on_progress)
        self.worker.result_row.connect(self._on_result_row)
        self.worker.finished_ok.connect(self._on_finished)
        self.worker.failed.connect(self._on_failed)
        self.worker.start()

        self.start_btn.setEnabled(False)
        self.stop_btn.setEnabled(True)
        self._update_status("Running...")
        self._log(f"Scan started: {plan.plan_id}, {len(points)} points")

    def _stop_scan(self) -> None:
        if self.worker:
            self.worker.cancel()
            self._log("Stop requested after current point...")
            self._update_status("Stopping...")

    def _on_progress(self, current: int, total: int) -> None:
        self.progress_bar.setRange(0, total)
        self.progress_bar.setValue(current)

    def _on_result_row(self, row: dict) -> None:
        self.results_table.add_result_row(row)
        self._last_results.append(row)

    def _on_finished(self, result: dict) -> None:
        s = result.get("summary", {})
        self._log(f"SCAN COMPLETE: OK={s.get('ok',0)} FAILED={s.get('failed',0)} SKIPPED={s.get('skipped',0)}")
        self._log(f"Results: {result.get('csv_path', '')}")

        # Auto-save to experience DB
        saved = 0
        try:
            for row in self._last_results:
                if row.get("status") != "OK":
                    continue
                params = {}
                metrics = {}
                metric_set = set(METRIC_KEYS)
                standard = {"run_index", "status", "seconds", "error"}
                for key, val in row.items():
                    if key in standard or val == "" or val is None:
                        continue
                    try:
                        fval = float(val)
                    except (ValueError, TypeError):
                        continue
                    if key in metric_set:
                        metrics[key] = fval
                    else:
                        params[key] = fval
                if params and metrics:
                    self._experience_db.add_run(
                        plan_id=self._plan.plan_id,
                        params=params,
                        metrics=metrics,
                        topology=self._plan.topology,
                        model_path=self._plan.model_path,
                        solve_time_s=float(row.get("seconds", 0) or 0),
                    )
                    saved += 1
            stats = self._experience_db.get_stats()
            self._log(f"Experience DB: saved {saved} runs (total: {stats['total_runs']})")
        except Exception as exc:
            self._log(f"Experience DB save warning: {exc}")

        self.start_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)
        self.recommend_btn.setEnabled(len(self._last_results) > 0)
        self._update_status("Complete")

    def _on_failed(self, error: str) -> None:
        self._log(f"SCAN FAILED:\n{error}")
        QMessageBox.critical(self, "Scan Failed", error)
        self.start_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)
        self._update_status("Failed")

    def _show_recommendations(self) -> None:
        """Show recommendation dialog based on last scan results."""
        if not self._last_results:
            QMessageBox.information(self, "No Data", "No scan results available for recommendation.")
            return

        # Find best result by efficiency
        best = None
        best_eff = -1
        for row in self._last_results:
            if row.get("status") != "OK":
                continue
            eff = row.get("efficiency_pct", "")
            try:
                eff_val = float(eff)
                if eff_val > best_eff:
                    best_eff = eff_val
                    best = row
            except (ValueError, TypeError):
                continue

        if best is None:
            QMessageBox.information(self, "No Data", "No valid results for recommendation.")
            return

        # Extract params and metrics from best result
        params = {}
        metrics = {}
        metric_set = set(METRIC_KEYS)
        standard = {"run_index", "status", "seconds", "error"}
        for key, val in best.items():
            if key in standard or val == "" or val is None:
                continue
            try:
                fval = float(val)
            except (ValueError, TypeError):
                continue
            if key in metric_set:
                metrics[key] = fval
            else:
                params[key] = fval

        # Get recommendation from experience DB
        rec = self._experience_db.recommend_next_round(
            params=params,
            metrics=metrics,
            target_metric="efficiency_pct",
            direction="maximize",
            topology=self._plan.topology,
        )

        # Build dialog text
        lines = []
        lines.append(f"Best result: efficiency={best_eff:.2f}%")
        lines.append(f"Parameters: {', '.join(f'{k}={v}' for k, v in params.items())}")
        lines.append("")
        lines.append(f"Recommendation status: {rec.get('status', 'unknown')}")
        lines.append(f"Similar runs in DB: {rec.get('similar_runs_used', 0)}")
        lines.append("")

        if rec.get("status") == "ok":
            lines.append("Parameter recommendations:")
            for param, info in rec.get("recommendations", {}).items():
                if info.get("status") == "ok":
                    direction = "INCREASE" if info.get("recommend_increase") else "DECREASE"
                    lines.append(f"  {param}: {direction}")
                    lines.append(f"    Current: {info.get('current_value')}")
                    lines.append(f"    Suggested range: {info.get('suggested_range')}")
                    lines.append(f"    Trend slope: {info.get('trend_slope')}")
                    lines.append(f"    Data points: {info.get('data_points')}")
                else:
                    lines.append(f"  {param}: {info.get('message', 'insufficient data')}")
        else:
            lines.append(rec.get("message", "No recommendation available."))

        lines.append("")
        lines.append("Note: Recommendations are based on trend analysis of accumulated")
        lines.append("simulation results. Accumulate more data for better accuracy.")

        QMessageBox.information(self, "Next Round Recommendation", "\n".join(lines))


# ---------------------------------------------------------------------------
# Entry point with 5-layer crash protection
# ---------------------------------------------------------------------------

def _excepthook(exc_type, exc_value, exc_tb):
    """Global exception handler (Layer 1)."""
    import traceback as tb
    msg = "".join(tb.format_exception(exc_type, exc_value, exc_tb))
    log_path = os.path.expanduser("~/pcb_afm_sim_error.log")
    try:
        with open(log_path, "a", encoding="utf-8") as f:
            f.write(f"\n=== {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===\n{msg}")
    except Exception:
        pass
    try:
        QMessageBox.critical(None, "Program Error", str(exc_value))
    except Exception:
        pass


def main() -> int:
    # Layer 1: global exception hook
    sys.excepthook = _excepthook

    app = QApplication(sys.argv)
    app.setStyle("Fusion")

    # Layer 2: don't quit on last window closed
    app.setQuitOnLastWindowClosed(False)

    # Layer 5: keep MotorCAD solver alive (handled in worker)

    window = MainWindow()
    window.show()

    return app.exec()


if __name__ == "__main__":
    sys.exit(main())
