"""自定义模板结果面板（Step 6 重构）。"""

import threading
import time
from tkinter import messagebox
import tkinter as tk
import ttkbootstrap as ttk

import colour
import numpy as np

from app.data_range_converter import convert_pattern_params
from app.views.modern_styles import get_theme_palette

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from pqAutomationApp import PQAutomationApp


def create_custom_template_result_panel(self: "PQAutomationApp"):
    """创建客户模板结果显示区域（黑底表格）"""
    self.custom_result_frame = ttk.LabelFrame(
        self.custom_template_tab_frame, text="客户模板结果显示"
    )
    self.custom_result_frame.pack(
        side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5
    )

    table_container = tk.Frame(
        self.custom_result_frame,
        highlightthickness=1,
    )
    table_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
    self.custom_result_table_container = table_container

    _apply_custom_result_theme(self)

    columns = (
        "Pattern",
        "No.",
        "X",
        "Y",
        "Z",
        "x",
        "y",
        "Lv",
        "u'",
        "v'",
        "Tcp",
        "duv",
        "λd/λc",
        "Pe"
    )

    self.custom_result_tree = ttk.Treeview(
        table_container,
        columns=columns,
        show="headings",
        height=4,
        style="CustomResult.Treeview",
    )

    column_widths = {
        "Pattern": 90,
        "No.": 60,
        "X": 80,
        "Y": 80,
        "Z": 80,
        "x": 80,
        "y": 80,
        "Lv": 80,
        "u'": 80,
        "v'": 80,
        "Tcp": 90,
        "duv": 80,
        "λd/λc": 95,
        "Pe": 80,
    }

    for col in columns:
        self.custom_result_tree.heading(col, text=col)
        self.custom_result_tree.column(
            col,
            width=column_widths.get(col, 80),
            minwidth=60,
            anchor=tk.CENTER,
            stretch=False,
        )

    y_scroll = ttk.Scrollbar(
        table_container,
        orient=tk.VERTICAL,
        command=self.custom_result_tree.yview,
    )
    x_scroll = ttk.Scrollbar(
        table_container,
        orient=tk.HORIZONTAL,
        command=self.custom_result_tree.xview,
    )

    self.custom_result_tree.configure(
        yscrollcommand=y_scroll.set,
        xscrollcommand=x_scroll.set,
    )

    self.custom_result_tree.grid(row=0, column=0, sticky="nsew")
    y_scroll.grid(row=0, column=1, sticky="ns")
    x_scroll.grid(row=1, column=0, sticky="ew")

    # 右键菜单：复制全部数据（Excel 可直接按行列粘贴）
    self.custom_result_menu = tk.Menu(self.root, tearoff=0)
    self.custom_result_menu.add_command(
        label="复制全部数据",
        command=self.copy_custom_result_table,
    )
    self.custom_result_menu.add_command(
        label="单步测试",
        command=self.start_custom_row_single_step,
    )
    self.custom_result_menu.add_separator()
    self.custom_result_menu.add_command(
        label="生成模板",
        command=self.export_custom_template_excel,
    )
    self.custom_result_menu.add_command(
        label="生成图表",
        command=self.export_custom_template_charts,
    )
    self.custom_result_tree.bind("<Button-3>", self.show_custom_result_context_menu)

    table_container.grid_rowconfigure(0, weight=1)
    table_container.grid_columnconfigure(0, weight=1)


def _apply_custom_result_theme(self: "PQAutomationApp"):
    palette = get_theme_palette()
    container = getattr(self, "custom_result_table_container", None)
    if container is not None:
        container.configure(
            bg=palette["input_bg"],
            highlightbackground=palette["border"],
            highlightcolor=palette["border"],
        )

    style = ttk.Style()
    style.configure(
        "CustomResult.Treeview",
        background=palette["input_bg"],
        fieldbackground=palette["input_bg"],
        foreground=palette["input_fg"],
        rowheight=28,
        borderwidth=0,
    )
    style.configure(
        "CustomResult.Treeview.Heading",
        background=palette["surface_alt_bg"],
        foreground=palette["muted_fg"],
        font=("Microsoft YaHei", 10, "bold"),
        relief="flat",
    )
    style.map(
        "CustomResult.Treeview",
        background=[("selected", palette["select_bg"])],
        foreground=[("selected", palette["select_fg"])],
    )
    style.map(
        "CustomResult.Treeview.Heading",
        background=[("active", palette["surface_hover_bg"])],
    )


def refresh_custom_template_theme(self: "PQAutomationApp"):
    """刷新客户模板结果表的主题色。"""
    _apply_custom_result_theme(self)


def _set_custom_template_tab_visible(self: "PQAutomationApp", visible: bool):
    """控制客户模板结果 TAB 的显示与隐藏。"""
    if not hasattr(self, "chart_notebook") or not hasattr(self, "custom_template_tab_frame"):
        return

    tab_id = str(self.custom_template_tab_frame)
    current_tabs = list(self.chart_notebook.tabs())

    if visible:
        if tab_id not in current_tabs:
            self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示")
        self.chart_notebook.select(self.custom_template_tab_frame)
        return

    if tab_id in current_tabs:
        current_selected = self.chart_notebook.select()
        self.chart_notebook.forget(self.custom_template_tab_frame)
        remaining_tabs = list(self.chart_notebook.tabs())
        if current_selected == tab_id and remaining_tabs:
            self.chart_notebook.select(remaining_tabs[0])


def show_custom_result_context_menu(self: "PQAutomationApp", event):
    """显示客户模板结果右键菜单"""
    if not hasattr(self, "custom_result_tree") or not hasattr(
        self, "custom_result_menu"
    ):
        return

    if self.testing:
        # 测试进行中锁定客户模板结果表，禁止右键菜单。
        return

    row_id = self.custom_result_tree.identify_row(event.y)
    if row_id:
        self.custom_result_tree.selection_set(row_id)
        self.custom_result_tree.focus(row_id)

    has_rows = len(self.custom_result_tree.get_children()) > 0
    has_selection = len(self.custom_result_tree.selection()) > 0
    can_single_step = (
        has_selection
        and self.ca is not None
        and self.signal_service.is_connected
        and not self.testing
    )
    try:
        self.custom_result_menu.entryconfigure(
            0,
            state=("normal" if has_rows else "disabled"),
        )
        self.custom_result_menu.entryconfigure(
            1,
            state=("normal" if can_single_step else "disabled"),
        )
        self.custom_result_menu.entryconfigure(
            3,
            state=("normal" if has_rows else "disabled"),
        )
        self.custom_result_menu.entryconfigure(
            4,
            state=("normal" if has_rows else "disabled"),
        )
        self.custom_result_menu.tk_popup(event.x_root, event.y_root)
    finally:
        self.custom_result_menu.grab_release()


def set_custom_result_table_locked(self: "PQAutomationApp", locked):
    """锁定/解锁客户模板结果表（测试期间禁选择、禁右键）"""
    if not hasattr(self, "custom_result_tree"):
        return

    try:
        self.custom_result_tree.configure(selectmode=("none" if locked else "browse"))
    except Exception:
        pass


def start_custom_row_single_step(self: "PQAutomationApp"):
    """单步测试当前选中行：发送该行 pattern 并覆盖该行测量结果"""
    if not hasattr(self, "custom_result_tree"):
        return

    if self.ca is None or not self.signal_service.is_connected:
        messagebox.showerror("错误", "请先连接CA410和信号发生器")
        return

    if self.testing:
        messagebox.showinfo("提示", "测试进行中，无法执行单步测试")
        return

    selected = self.custom_result_tree.selection()
    if not selected:
        messagebox.showinfo("提示", "请先选中一行再执行单步测试")
        return

    item_id = selected[0]
    values = self.custom_result_tree.item(item_id, "values")
    if not values:
        messagebox.showinfo("提示", "选中行没有有效数据")
        return

    row_no = None
    if len(values) > 1:
        try:
            row_no = int(float(values[1]))
        except Exception:
            row_no = None

    if row_no is None or row_no <= 0:
        children = list(self.custom_result_tree.get_children())
        row_no = children.index(item_id) + 1 if item_id in children else 1

    _clear_custom_result_row(self, item_id, row_no)

    threading.Thread(
        target=_run_custom_row_single_step,
        args=(self, item_id, row_no),
        daemon=True,
    ).start()


def _clear_custom_result_row(self: "PQAutomationApp", item_id, row_no):
    """单步测试开始前清空指定行的测量数据"""
    if not hasattr(self, "custom_result_tree"):
        return

    old_values = list(self.custom_result_tree.item(item_id, "values"))
    pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}"

    cleared_values = (
        pattern_name,
        row_no,
        "---",
        "---",
        "---",
        "---",
        "---",
        "---",
        "---",
        "---",
        "---",
        "---",
        "---",
        "---",
    )

    self.custom_result_tree.item(item_id, values=cleared_values)
    self.custom_result_tree.see(item_id)


def _run_custom_row_single_step(self: "PQAutomationApp", item_id, row_no):
    """后台执行客户模板单步测试"""
    try:
        self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...")
        self.log_gui.log(f"开始单步测试第 {row_no} 行", level="info")

        self.config.set_current_pattern("custom")

        # 与批量 custom 测试保持一致：根据当前 SDR 配置转换 pattern 数据。
        import copy

        data_range = self.sdr_data_range_var.get()
        original_params = copy.deepcopy(self.config.default_pattern_temp["pattern_params"])
        converted_params = convert_pattern_params(
            pattern_params=original_params,
            data_range=data_range,
            verbose=False,
        )

        temp_config = self.config.get_temp_config_with_converted_params(
            mode="custom",
            converted_params=converted_params,
        )

        if row_no > len(converted_params):
            self.log_gui.log(f"行号超出 pattern 范围: {row_no}/{len(converted_params)}", level="error")
            self._dispatch_ui(self.status_var.set, "单步测试失败：行号超范围")
            return

        pattern_param = converted_params[row_no - 1]
        self.signal_service.apply_and_run(temp_config, pattern_param)

        time.sleep(self.pattern_settle_time)

        # 测量：显示模式1读取 Tcp/duv/Lv，显示模式8读取 λd/Pe/Lv 与 XYZ。
        tcp, duv, lv, _, _, _ = self.read_ca_tcp_duv()

        lambda_d, pe, lv, X, Y, Z = self.read_ca_lambda_pe()

        xy = colour.XYZ_to_xy(np.array([X, Y, Z]))
        u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(np.array([X, Y, Z]))

        row_data = {
            "X": X,
            "Y": Y,
            "Z": Z,
            "x": xy[0],
            "y": xy[1],
            "Lv": lv,
            "u_prime": u_prime,
            "v_prime": v_prime,
            "Tcp": tcp,
            "duv": duv,
            "lambda_d": lambda_d,
            "Pe": pe,
        }

        self._dispatch_ui(
            _update_custom_result_row, self, item_id, row_no, row_data
        )

        self.log_gui.log(f"第 {row_no} 行单步测试完成并已覆盖", level="success")
        self._dispatch_ui(self.status_var.set, f"第 {row_no} 行单步测试完成")

    except Exception as e:
        self.log_gui.log(f"单步测试失败: {str(e)}", level="error")
        self._dispatch_ui(self.status_var.set, "单步测试失败")


def _update_custom_result_row(self: "PQAutomationApp", item_id, row_no, result_data):
    """覆盖更新客户模板结果表中指定行"""

    def fmt(value, digits=4):
        if value is None:
            return "--"
        if isinstance(value, (int, float, np.floating)):
            # CA 返回异常哨兵值（如 -99999999）时，显示为占位符。
            if (not np.isfinite(value)) or value <= -99999998:
                return "---"
            return f"{value:.{digits}f}"
        try:
            numeric_value = float(value)
            if (not np.isfinite(numeric_value)) or numeric_value <= -99999998:
                return "---"
        except (TypeError, ValueError):
            pass
        return str(value)

    old_values = list(self.custom_result_tree.item(item_id, "values"))
    pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}"

    new_values = (
        pattern_name,
        row_no,
        fmt(result_data.get("X")),
        fmt(result_data.get("Y")),
        fmt(result_data.get("Z")),
        fmt(result_data.get("x")),
        fmt(result_data.get("y")),
        fmt(result_data.get("Lv"), 3),
        fmt(result_data.get("u_prime")),
        fmt(result_data.get("v_prime")),
        fmt(result_data.get("Tcp"), 1),
        fmt(result_data.get("duv"), 5),
        fmt(result_data.get("lambda_d"), 1),
        fmt(result_data.get("Pe"), 1),
    )

    self.custom_result_tree.item(item_id, values=new_values)


def copy_custom_result_table(self: "PQAutomationApp"):
    """复制客户模板结果表格到剪贴板（不含标题行/No./Pattern）"""
    if not hasattr(self, "custom_result_tree"):
        return

    items = self.custom_result_tree.get_children()
    if not items:
        messagebox.showinfo("提示", "当前没有可复制的数据")
        return

    lines = []
    columns = tuple(self.custom_result_tree["columns"])
    excluded_col_indexes = {
        idx
        for idx, col_name in enumerate(columns)
        if col_name in ("No.", "Pattern")
    }

    for item in items:
        values = self.custom_result_tree.item(item, "values")
        # 跳过 No. 和 Pattern 两列，只保留测量数据列。
        data_values = [
            v for idx, v in enumerate(values) if idx not in excluded_col_indexes
        ]
        row = [
            str(v).replace("\t", " ").replace("\n", " ")
            for v in data_values
        ]
        lines.append("\t".join(row))

    clipboard_text = "\n".join(lines)
    self.root.clipboard_clear()
    self.root.clipboard_append(clipboard_text)
    self.root.update_idletasks()

    if hasattr(self, "status_var"):
        self.status_var.set(f"已复制 {len(items)} 行客户模板数据到剪贴板")
    if hasattr(self, "log_gui"):
        self.log_gui.log(f"已复制客户模板表格数据（{len(items)} 行）", level="success")

def clear_custom_template_results(self: "PQAutomationApp"):
    """清空客户模板结果表格"""
    if not hasattr(self, "custom_result_tree"):
        return
    for item in self.custom_result_tree.get_children():
        self.custom_result_tree.delete(item)


def auto_expand_custom_result_view(self: "PQAutomationApp"):
    """当客户模板表格有数据时，自动扩展窗口以尽量完整显示所有列"""
    if not hasattr(self, "custom_result_tree"):
        return

    if len(self.custom_result_tree.get_children()) == 0:
        return

    try:
        self.root.update_idletasks()

        columns = tuple(self.custom_result_tree["columns"])
        columns_total_width = 0
        for col in columns:
            columns_total_width += int(self.custom_result_tree.column(col, "width"))

        left_panel_width = self.left_frame.winfo_width() if hasattr(self, "left_frame") else 180
        if left_panel_width <= 1:
            left_panel_width = 180

        # 列宽 + 左侧导航 + 滚动条/边框/外边距。
        target_width = int(left_panel_width + columns_total_width + 120)

        screen_max_width = max(900, self.root.winfo_screenwidth() - 40)
        target_width = min(target_width, screen_max_width)

        current_width = self.root.winfo_width()
        current_height = self.root.winfo_height()

        # 只扩不缩，避免用户窗口被反复改变。
        if target_width > current_width:
            self.root.geometry(f"{target_width}x{current_height}")
            self.root.update_idletasks()
    except Exception as e:
        if hasattr(self, "log_gui"):
            self.log_gui.log(f"自动扩展客户模板窗口失败: {str(e)}", level="error")


def append_custom_template_result(self: "PQAutomationApp", row_no, result_data):
    """追加一条客户模板结果到表格"""

    def fmt(value, digits=4):
        if value is None:
            return "--"
        if isinstance(value, (int, float, np.floating)):
            # CA 返回异常哨兵值（如 -99999999）时，显示为占位符。
            if (not np.isfinite(value)) or value <= -99999998:
                return "---"
            return f"{value:.{digits}f}"
        try:
            numeric_value = float(value)
            if (not np.isfinite(numeric_value)) or numeric_value <= -99999998:
                return "---"
        except (TypeError, ValueError):
            pass
        return str(value)

    row_values = (
        result_data.get("pattern_name", f"P {row_no}"),
        row_no,
        fmt(result_data.get("X")),
        fmt(result_data.get("Y")),
        fmt(result_data.get("Z")),
        fmt(result_data.get("x")),
        fmt(result_data.get("y")),
        fmt(result_data.get("Lv"), 3),
        fmt(result_data.get("u_prime")),
        fmt(result_data.get("v_prime")),
        fmt(result_data.get("Tcp"), 1),
        fmt(result_data.get("duv"), 5),
        fmt(result_data.get("lambda_d"), 1),
        fmt(result_data.get("Pe"), 1)
    )

    if hasattr(self, "custom_result_tree"):
        item_id = self.custom_result_tree.insert("", tk.END, values=row_values)
        # 新增数据后自动跳转到最新行。
        self.custom_result_tree.see(item_id)
        self.auto_expand_custom_result_view()


def start_custom_template_test(self: "PQAutomationApp"):
    """开始客户模板测试（SDR）"""

    if self.ca is None or not self.signal_service.is_connected:
        messagebox.showerror("错误", "请先连接CA410和信号发生器")
        return

    if self.testing:
        messagebox.showinfo("提示", "测试已在进行中")
        return

    if hasattr(self, "debug_container"):
        self.debug_container.pack_forget()

    self.testing = True
    self.start_btn.config(state=tk.DISABLED)
    self.stop_btn.config(state=tk.NORMAL)
    self.save_btn.config(state=tk.DISABLED)
    self.clear_config_btn.config(state=tk.DISABLED)
    self.custom_btn.config(state=tk.DISABLED)
    self.status_var.set("客户模板测试进行中...")

    self.log_gui.clear_log()
    self.clear_custom_template_results()

    confirm = messagebox.askyesno(
        "确认测试", "开始客户模板测试（SDR）？\n\n将采集并显示客户模板格式结果。"
    )

    if not confirm:
        self.testing = False
        self.start_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)
        self.clear_config_btn.config(state=tk.NORMAL)
        self.custom_btn.config(state=tk.NORMAL)
        self.status_var.set("测试已取消")
        self.set_custom_result_table_locked(False)
        _set_custom_template_tab_visible(self, False)
        return

    _set_custom_template_tab_visible(self, True)
    self.set_custom_result_table_locked(True)

    self.test_thread = threading.Thread(target=self.run_custom_sdr_test, args=([],))
    self.test_thread.daemon = True
    self.test_thread.start()

def update_custom_button_visibility(self: "PQAutomationApp"):
    """只在 SDR 测试时显示客户模版按钮"""
    if not hasattr(self, "custom_btn") or not hasattr(self, "test_type_var"):
        return
    if self.test_type_var.get() == "sdr_movie":
        if not self.custom_btn.winfo_manager():
            self.custom_btn.pack(side=tk.LEFT, padx=5)
    else:
        if self.custom_btn.winfo_manager():
            self.custom_btn.pack_forget()


# def fill_custom_result_test_data(self):
#     """填充 147 行客户模板测试数据（用于界面验证）"""
#     if not hasattr(self, "custom_result_tree"):
#         return

#     self.clear_custom_template_results()

#     pattern_names = []
#     if hasattr(self, "config") and hasattr(self.config, "get_temp_pattern_names"):
#         pattern_names = self.config.get_temp_pattern_names()

#     total_rows = 147
#     for i in range(1, total_rows + 1):
#         ratio = (i - 1) / (total_rows - 1) if total_rows > 1 else 0
#         row_data = {
#             "pattern_name": (
#                 pattern_names[i - 1] if i - 1 < len(pattern_names) else f"P {i}"
#             ),
#             "X": 0.8 + ratio * 120,
#             "Y": 0.9 + ratio * 135,
#             "Z": 1.1 + ratio * 145,
#             "x": 0.24 + ratio * 0.10,
#             "y": 0.26 + ratio * 0.10,
#             "Lv": 1.0 + ratio * 500,
#             "u_prime": 0.16 + ratio * 0.12,
#             "v_prime": 0.42 + ratio * 0.08,
#             "Tcp": 1800 + ratio * 12000,
#             "duv": -0.01 + ratio * 0.03,
#             "lambda_d": 430 + ratio * 200,
#             "Pe": 10 + ratio * 90,
#         }
#         self.append_custom_template_result(i, row_data)

#     if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
#         self.chart_notebook.select(self.custom_template_tab_frame)

#     if hasattr(self, "status_var"):
#         self.status_var.set("已填充 147 行客户模板测试数据")
#     if hasattr(self, "log_gui"):
#         self.log_gui.log("已填充 147 行客户模板测试数据", level="success")


def export_custom_template_excel(self: "PQAutomationApp"):
    """将客户模板结果表导出为 Excel 文件（14 列完整数据）"""
    if not hasattr(self, "custom_result_tree"):
        return

    items = self.custom_result_tree.get_children()
    if not items:
        messagebox.showinfo("提示", "当前没有可导出的数据")
        return

    import datetime
    from tkinter import filedialog

    default_name = (
        f"客户模板结果_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
    )
    save_path = filedialog.asksaveasfilename(
        title="保存客户模板 Excel 报告",
        defaultextension=".xlsx",
        filetypes=[("Excel 文件", "*.xlsx")],
        initialfile=default_name,
    )
    if not save_path:
        return

    try:
        from openpyxl import Workbook
        from openpyxl.styles import Alignment, Border, Font, PatternFill, Side

        wb = Workbook()
        ws = wb.active
        ws.title = "客户模板测试结果"

        columns = tuple(self.custom_result_tree["columns"])
        num_cols = len(columns)
        # 列字母辅助（A-N，共 14 列，全在单字母范围内）
        def col_letter(idx_1based):
            return chr(64 + idx_1based)

        last_col = col_letter(num_cols)

        # ---- 标题行 ----
        ws.merge_cells(f"A1:{last_col}1")
        ws["A1"] = "客户模板测试结果"
        ws["A1"].font = Font(name="微软雅黑", size=16, bold=True, color="FFFFFF")
        ws["A1"].fill = PatternFill(
            start_color="4472C4", end_color="4472C4", fill_type="solid"
        )
        ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
        ws.row_dimensions[1].height = 35

        # 写入测试时间
        ws.merge_cells(f"A2:{last_col}2")
        ws["A2"] = (
            f"测试时间：{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
        )
        ws["A2"].font = Font(name="微软雅黑", size=10, color="CCCCCC")
        ws["A2"].fill = PatternFill(
            start_color="2F2F2F", end_color="2F2F2F", fill_type="solid"
        )
        ws["A2"].alignment = Alignment(horizontal="left", vertical="center")
        ws.row_dimensions[2].height = 20

        # ---- 表头行 ----
        thin = Side(style="thin")
        border = Border(left=thin, right=thin, top=thin, bottom=thin)
        header_font = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF")
        header_fill = PatternFill(
            start_color="70AD47", end_color="70AD47", fill_type="solid"
        )
        header_align = Alignment(
            horizontal="center", vertical="center", wrap_text=True
        )

        for col_idx, col_name in enumerate(columns, start=1):
            cell = ws.cell(row=3, column=col_idx, value=col_name)
            cell.font = header_font
            cell.fill = header_fill
            cell.alignment = header_align
            cell.border = border
        ws.row_dimensions[3].height = 22

        # ---- 数据行 ----
        data_font = Font(name="微软雅黑", size=10)
        data_align = Alignment(horizontal="center", vertical="center")
        # 数值列（跳过 Pattern 和 No.）以 4 位小数格式输出
        numeric_col_indices = set(range(2, num_cols))

        for row_offset, item in enumerate(items):
            row_num = 4 + row_offset
            values = self.custom_result_tree.item(item, "values")
            for col_idx, value in enumerate(values, start=1):
                cell = ws.cell(row=row_num, column=col_idx)
                cell.font = data_font
                cell.alignment = data_align
                cell.border = border
                # 占位符保持文本，非占位符数值列尝试转为浮点数
                if (
                    col_idx - 1 in numeric_col_indices
                    and str(value) not in ("---", "--", "")
                ):
                    try:
                        cell.value = float(value)
                        cell.number_format = "0.0000"
                    except (ValueError, TypeError):
                        cell.value = value
                else:
                    cell.value = value
            ws.row_dimensions[row_num].height = 20

        # ---- 列宽 ----
        col_widths = {
            "Pattern": 14,
            "No.": 8,
            "X": 11,
            "Y": 11,
            "Z": 11,
            "x": 10,
            "y": 10,
            "Lv": 10,
            "u'": 10,
            "v'": 10,
            "Tcp": 12,
            "duv": 10,
            "\u03bbd/\u03bbc": 12,
            "Pe": 10,
        }
        for col_idx, col_name in enumerate(columns, start=1):
            ws.column_dimensions[col_letter(col_idx)].width = col_widths.get(
                col_name, 11
            )

        wb.save(save_path)

        if hasattr(self, "status_var"):
            self.status_var.set("已导出客户模板 Excel 报告")
        if hasattr(self, "log_gui"):
            self.log_gui.log(
                f"已导出客户模板 Excel 报告: {save_path}", level="success"
            )
        messagebox.showinfo("成功", f"Excel 报告已保存到：\n{save_path}")

    except Exception as e:
        if hasattr(self, "log_gui"):
            self.log_gui.log(f"导出 Excel 失败: {str(e)}", level="error")
        messagebox.showerror("错误", f"导出失败：{str(e)}")


def export_custom_template_charts(self: "PQAutomationApp"):
    """生成客户模板图表：xy 色度散点图 + Lv 亮度曲线图，保存为 PNG"""
    if not hasattr(self, "custom_result_tree"):
        return

    items = self.custom_result_tree.get_children()
    if not items:
        messagebox.showinfo("提示", "当前没有可绘制的数据")
        return

    import datetime
    from tkinter import filedialog

    default_name = (
        f"客户模板图表_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
    )
    save_path = filedialog.asksaveasfilename(
        title="保存客户模板图表",
        defaultextension=".png",
        filetypes=[("PNG 图片", "*.png")],
        initialfile=default_name,
    )
    if not save_path:
        return

    try:
        import matplotlib.pyplot as plt

        columns = tuple(self.custom_result_tree["columns"])
        col_idx_map = {col: idx for idx, col in enumerate(columns)}

        pattern_names, x_vals, y_vals, lv_vals = [], [], [], []
        for item in items:
            vals = self.custom_result_tree.item(item, "values")
            pattern_names.append(str(vals[col_idx_map.get("Pattern", 0)]))
            for container, key, fallback in (
                (x_vals, "x", 5),
                (y_vals, "y", 6),
                (lv_vals, "Lv", 7),
            ):
                raw = vals[col_idx_map.get(key, fallback)]
                try:
                    v = float(raw)
                    container.append(v if np.isfinite(v) and v > -99999998 else None)
                except (ValueError, TypeError):
                    container.append(None)

        # ---- 绘图 ----
        fig, (ax_xy, ax_lv) = plt.subplots(
            1, 2, figsize=(16, 7), facecolor="#1a1a2e"
        )

        # ── 左图：xy 色度散点图 ──
        ax_xy.set_facecolor("#0f0f23")
        ax_xy.set_xlim(0, 0.8)
        ax_xy.set_ylim(0, 0.9)
        ax_xy.set_xlabel("x", color="#cccccc", fontsize=11)
        ax_xy.set_ylabel("y", color="#cccccc", fontsize=11)
        ax_xy.set_title("xy 色度图", color="#ffffff", fontsize=13, fontweight="bold")
        ax_xy.tick_params(colors="#aaaaaa", which="both")
        for spine in ax_xy.spines.values():
            spine.set_color("#444444")
        ax_xy.grid(color="#333333", linestyle="--", linewidth=0.5, alpha=0.7)

        # D65 白点标注
        ax_xy.scatter(
            [0.3127], [0.3290],
            c="#ffffff", s=100, zorder=5,
            marker="+", linewidths=2, label="D65",
        )

        valid_pairs = [
            (x, y, i)
            for i, (x, y) in enumerate(zip(x_vals, y_vals))
            if x is not None and y is not None
        ]
        if valid_pairs:
            xs, ys, idxs = zip(*valid_pairs)
            sc = ax_xy.scatter(
                xs, ys,
                c=idxs,
                cmap="plasma",
                s=60, zorder=4,
                edgecolors="#cccccc", linewidths=0.5, alpha=0.9,
            )
            cbar = fig.colorbar(sc, ax=ax_xy, pad=0.01)
            cbar.set_label("测量序号", color="#cccccc", fontsize=10)
            cbar.ax.yaxis.set_tick_params(color="#aaaaaa")
            plt.setp(cbar.ax.yaxis.get_ticklabels(), color="#aaaaaa")

        ax_xy.legend(
            fontsize=9,
            facecolor="#2a2a3e",
            edgecolor="#555555",
            labelcolor="#cccccc",
        )

        # ── 右图：Lv 亮度曲线 ──
        ax_lv.set_facecolor("#0f0f23")
        ax_lv.set_title(
            "Lv 亮度曲线", color="#ffffff", fontsize=13, fontweight="bold"
        )
        ax_lv.set_xlabel("测量序号", color="#cccccc", fontsize=11)
        ax_lv.set_ylabel("Lv (cd/m²)", color="#cccccc", fontsize=11)
        ax_lv.tick_params(colors="#aaaaaa", which="both")
        for spine in ax_lv.spines.values():
            spine.set_color("#444444")
        ax_lv.grid(color="#333333", linestyle="--", linewidth=0.5, alpha=0.7)

        valid_lv = [(i + 1, lv) for i, lv in enumerate(lv_vals) if lv is not None]
        if valid_lv:
            seq, lvs = zip(*valid_lv)
            ax_lv.plot(
                seq, lvs,
                color="#4fc3f7", linewidth=1.5,
                marker="o", markersize=4,
                markerfacecolor="#ff8c00", markeredgecolor="#ff8c00",
            )
            ax_lv.fill_between(seq, lvs, alpha=0.15, color="#4fc3f7")

        plt.tight_layout(pad=2.0)
        fig.savefig(save_path, dpi=200, bbox_inches="tight",
                    facecolor=fig.get_facecolor())
        plt.close(fig)

        if hasattr(self, "status_var"):
            self.status_var.set("已生成客户模板图表")
        if hasattr(self, "log_gui"):
            self.log_gui.log(
                f"已生成客户模板图表: {save_path}", level="success"
            )
        messagebox.showinfo("成功", f"图表已保存到：\n{save_path}")

    except Exception as e:
        if hasattr(self, "log_gui"):
            self.log_gui.log(f"生成图表失败: {str(e)}", level="error")
        messagebox.showerror("错误", f"生成图表失败：{str(e)}")


class CustomTemplatePanelMixin:
    """由 tools/refactor_to_mixins.py 自动生成。
    把本模块的自由函数挂到 PQAutomationApp 上，便于 F12 跳转与类型推断。
    """
    create_custom_template_result_panel = create_custom_template_result_panel
    _set_custom_template_tab_visible = _set_custom_template_tab_visible
    show_custom_result_context_menu = show_custom_result_context_menu
    set_custom_result_table_locked = set_custom_result_table_locked
    start_custom_row_single_step = start_custom_row_single_step
    _clear_custom_result_row = _clear_custom_result_row
    _run_custom_row_single_step = _run_custom_row_single_step
    _update_custom_result_row = _update_custom_result_row
    copy_custom_result_table = copy_custom_result_table
    clear_custom_template_results = clear_custom_template_results
    auto_expand_custom_result_view = auto_expand_custom_result_view
    append_custom_template_result = append_custom_template_result
    start_custom_template_test = start_custom_template_test
    update_custom_button_visibility = update_custom_button_visibility
    export_custom_template_excel = export_custom_template_excel
    export_custom_template_charts = export_custom_template_charts
    refresh_custom_template_theme = refresh_custom_template_theme
