"""设备连接管理（UCD323 / CA410）。

重构目标
---------
- 用 :class:`ConnectionController` 类封装连接生命周期，替代旧的
  "把模块级函数当类方法装到 PQAutomationApp 上" 的写法。
- 拆掉 ``check_port_connection(is_ucd)`` 的布尔旗参数反模式，
  分离为 ``connect_ucd`` / ``connect_ca`` 两条独立路径。
- UCD 经由 :class:`UCD323Device` + :class:`EventBus` 管理；
  指示灯由 GUI 订阅带 :class:`DeviceKind` 的 :class:`ConnectionChanged` 事件更新。

模块底部仍保留 7 个旧名字函数作为薄兼容层（``check_com_connections``
等），供 ``pqAutomationApp.PQAutomationApp`` 类体继续以同名属性挂接，
保证调用点（按钮 command、_dispatch_ui 引用）零修改。
"""

from __future__ import annotations

import threading
import time
from tkinter import messagebox
from typing import TYPE_CHECKING

from app.ucd import ConnectionChanged, DeviceKind, DeviceInfo, UCD323Device, UcdError
from drivers.caSerail import CASerail
from app.views.modern_styles import get_theme_palette

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from pqAutomationApp import PQAutomationApp


if TYPE_CHECKING:
    from app.ucd import EventBus


# ─── ConnectionController ────────────────────────────────────────


class ConnectionController:
    """统一管理 CA410 / UCD323 的连接、断开与可用端口枚举。

    设计要点：
    - 不持有 :mod:`tkinter` 控件；所有 GUI 更新由订阅事件总线后回调完成。
    - UCD 连接经由 :class:`UCD323Device`，自动发布
      :class:`ConnectionChanged` 事件。
    - CA 连接同样发布 :class:`ConnectionChanged` 事件（带 ``serial=None``，
      指示器订阅时只看 ``connected`` 字段）。
    """

    def __init__(self, app):
        self._app = app
        self._bus: "EventBus" = app.event_bus
        self._device: "UCD323Device" = app.ucd_device
        # 旧 GUI 仍用 self.ca 直接访问 CA410 对象，过渡期保留同步赋值。
        if not hasattr(app, "ca") or app.ca is None:
            self._app.ca = None

    # -- 端口枚举 ------------------------------------------------

    def list_ucd_devices(self) -> list[str]:
        """返回 SDK 给出的设备显示字符串列表。"""
        try:
            return self._device.search_devices()
        except Exception as exc:  # noqa: BLE001
            self._log(f"枚举 UCD 设备失败: {exc}", level="error")
            return []

    def list_com_ports(self) -> list[str]:
        try:
            import serial.tools.list_ports

            return [p.device for p in serial.tools.list_ports.comports()]
        except Exception as exc:  # noqa: BLE001
            self._log(f"获取COM端口列表出错: {exc}", level="error")
            return []

    # -- UCD 连接 ------------------------------------------------

    def _pick_ucd_device(self, ucd_list: list[str], display: str) -> str | None:
        """Prefer devices not marked in-use; fall back to full list."""
        usable = [d for d in ucd_list if "(in use)" not in d.lower()]
        if display in usable:
            return display
        if usable:
            return usable[0]
        if display in ucd_list:
            return display
        return ucd_list[0] if ucd_list else None

    def _refresh_ucd_selection(self, display: str) -> tuple[list[str], str | None]:
        """Rescan devices and return (device list, valid selection or None)."""
        ucd_list = self.list_ucd_devices()
        return ucd_list, self._pick_ucd_device(ucd_list, display)

    def _sync_ucd_combo(self, ucd_list: list[str], selected: str) -> None:
        app = self._app
        app.ucd_list_combo.config(values=ucd_list)
        app.ucd_list_var.set(selected)
        app.update_config()

    def _prepare_ucd_reconnect(self) -> None:
        """Release stale SDK/native handles before a new open attempt."""
        if self._device.state.name != "CLOSED":
            try:
                self._device.close()
                return
            except Exception as exc:  # noqa: BLE001
                self._log(f"关闭旧 UCD 会话失败，将强制重置: {exc}", level="info")
        try:
            self._device.force_reset()
        except Exception as exc:  # noqa: BLE001
            self._log(f"强制重置 UCD SDK 失败: {exc}", level="error")

    def connect_ucd(self, display: str, *, rescan: bool = True) -> bool:
        """打开指定 UCD 设备。成功返回 True。"""
        if rescan:
            _, display = self._refresh_ucd_selection(display)
        if not display:
            self._log("未检测到 UCD 设备，请确认电源已打开后重试", level="error")
            return False

        self._prepare_ucd_reconnect()

        last_error: Exception | None = None
        for attempt in range(2):
            try:
                self._device.open(DeviceInfo.parse(display))
                return True
            except UcdError as exc:
                last_error = exc
                self._log(
                    f"设备 {display} 连接失败 ({attempt + 1}/2): {exc}",
                    level="error",
                )
            except Exception as exc:  # noqa: BLE001
                last_error = exc
                self._log(
                    f"UCD323 连接异常 ({attempt + 1}/2): {exc}",
                    level="error",
                )

            if attempt == 0:
                try:
                    self._device.force_reset()
                except Exception as exc:  # noqa: BLE001
                    self._log(f"重连前强制重置 UCD 失败: {exc}", level="error")
                if rescan:
                    _, display = self._refresh_ucd_selection(display)
                    if not display:
                        return False

        if last_error is not None:
            self._log(f"UCD323 重连失败: {last_error}", level="error")
        return False

    def disconnect_ucd(self) -> None:
        try:
            self._device.close()
        except Exception:  # noqa: BLE001
            try:
                self._device.force_reset()
            except Exception:  # noqa: BLE001
                pass
        self._log("UCD连接已断开", level="info")

    # -- CA 连接 -------------------------------------------------

    def connect_ca(self) -> bool:
        """打开 CA410。成功返回 True 并设置 ``app.ca``。"""
        if self._app.ca is not None:
            try:
                self._app.ca.close()
            except Exception:  # noqa: BLE001
                pass
            self._app.ca = None

        try:
            ca = CASerail()
            ca.open(self._app.config.device_config["ca_com"], 19200, 7, "E", 2)
            if not ca.set_all_Display():
                self._log(
                    f"端口 {self._app.config.device_config['ca_com']} 异常，色温仪连接失败",
                    level="error",
                )
                ca.close()
                return False
            ca.setSynchMode(3)
            ca.setMeasureSpeed(1)
            time.sleep(0.5)
            ca.setZeroCalibration()
            channel_value = self._app.ca_channel_var.get()
            ca.setChannel(f"{int(channel_value):02d}")
            self._app.ca = ca
            self._bus.publish(ConnectionChanged(DeviceKind.CA, True, None))
            return True
        except Exception as exc:  # noqa: BLE001
            self._log(f"CA410 连接失败: {exc}", level="error")
            return False

    def disconnect_ca(self) -> None:
        if self._app.ca is None:
            return
        try:
            self._app.ca.close()
        except Exception:  # noqa: BLE001
            pass
        self._app.ca = None
        self._bus.publish(ConnectionChanged(DeviceKind.CA, False, None))
        self._log("CA连接已断开", level="info")

    # -- 一次性入口 ----------------------------------------------

    def check_all_async(self) -> None:
        """异步并联检测 UCD + CA，通过 ``_dispatch_ui`` 回主线程更新 UI。"""
        app = self._app
        self._set_connect_widgets_state("disabled")
        app.status_var.set("正在检测连接...")
        app.root.update()

        def worker():
            try:
                ucd_list, selected = self._refresh_ucd_selection(app.ucd_list_var.get())
                app._dispatch_ui(self._sync_ucd_combo, ucd_list, selected or "")
                ucd_ok = False
                if selected:
                    ucd_ok = self.connect_ucd(selected, rescan=False)
                app._dispatch_ui(
                    app.update_connection_indicator,
                    app.ucd_status_indicator, ucd_ok,
                )
                ca_ok = self.connect_ca()
                app._dispatch_ui(
                    app.update_connection_indicator,
                    app.ca_status_indicator, ca_ok,
                )
                app._dispatch_ui(app.status_var.set, "连接检测完成")
                app._dispatch_ui(self._enable_widgets)
            except Exception as exc:  # noqa: BLE001
                app._dispatch_ui(app.log_gui.log, f"连接检测出错: {exc}")
                app._dispatch_ui(self._enable_widgets)

        threading.Thread(target=worker, daemon=True).start()

    def check_ucd_async(self) -> None:
        """仅异步连接 UCD323。"""
        app = self._app
        self._set_connect_widgets_state("disabled")
        app.status_var.set("正在连接 UCD323...")
        app.root.update()

        def worker():
            try:
                ucd_list, selected = self._refresh_ucd_selection(app.ucd_list_var.get())
                app._dispatch_ui(self._sync_ucd_combo, ucd_list, selected or "")
                if not selected:
                    app._dispatch_ui(
                        app.update_connection_indicator,
                        app.ucd_status_indicator,
                        False,
                    )
                    app._dispatch_ui(app.status_var.set, "未检测到 UCD 设备")
                    app._dispatch_ui(self._enable_widgets)
                    return

                ucd_ok = self.connect_ucd(selected, rescan=False)
                app._dispatch_ui(
                    app.update_connection_indicator,
                    app.ucd_status_indicator,
                    ucd_ok,
                )
                app._dispatch_ui(
                    app.status_var.set,
                    "UCD323 连接成功" if ucd_ok else "UCD323 连接失败",
                )
                app._dispatch_ui(self._enable_widgets)
            except Exception as exc:  # noqa: BLE001
                app._dispatch_ui(app.log_gui.log, f"UCD323 连接出错: {exc}")
                app._dispatch_ui(self._enable_widgets)

        threading.Thread(target=worker, daemon=True).start()

    def check_ca_async(self) -> None:
        """仅异步连接 CA410。"""
        app = self._app
        self._set_connect_widgets_state("disabled")
        app.status_var.set("正在连接 CA410...")
        app.root.update()

        def worker():
            try:
                ca_ok = self.connect_ca()
                app._dispatch_ui(
                    app.update_connection_indicator,
                    app.ca_status_indicator,
                    ca_ok,
                )
                app._dispatch_ui(
                    app.status_var.set,
                    "CA410 连接成功" if ca_ok else "CA410 连接失败",
                )
                app._dispatch_ui(self._enable_widgets)
            except Exception as exc:  # noqa: BLE001
                app._dispatch_ui(app.log_gui.log, f"CA410 连接出错: {exc}")
                app._dispatch_ui(self._enable_widgets)

        threading.Thread(target=worker, daemon=True).start()

    def disconnect_all(self) -> None:
        try:
            self.disconnect_ucd()
            self.disconnect_ca()
            self._enable_widgets()
            self._app.refresh_connection_indicators()
            self._app.status_var.set("串口连接已断开")
        except Exception as exc:  # noqa: BLE001
            self._log(f"断开连接时发生错误: {exc}", level="info")
            messagebox.showerror("错误", f"断开连接失败: {exc}")

    def disconnect_ucd_only(self) -> None:
        try:
            self.disconnect_ucd()
            self._app.refresh_connection_indicators()
            self._app.status_var.set("UCD323 已断开")
        except Exception as exc:  # noqa: BLE001
            self._log(f"断开 UCD323 时发生错误: {exc}", level="info")
            messagebox.showerror("错误", f"断开 UCD323 失败: {exc}")

    def disconnect_ca_only(self) -> None:
        try:
            self.disconnect_ca()
            self._app.refresh_connection_indicators()
            self._app.status_var.set("CA410 已断开")
        except Exception as exc:  # noqa: BLE001
            self._log(f"断开 CA410 时发生错误: {exc}", level="info")
            messagebox.showerror("错误", f"断开 CA410 失败: {exc}")

    def refresh_ports(self) -> None:
        """刷新 UCD + COM 端口下拉框；指示器复位。"""
        app = self._app
        com_ports = self.list_com_ports()
        ucd_list = self.list_ucd_devices()
        if not ucd_list or all("(in use)" in d.lower() for d in ucd_list):
            try:
                self._device.force_reset()
            except Exception as exc:  # noqa: BLE001
                self._log(f"刷新时重置 UCD SDK: {exc}", level="info")
            ucd_list = self.list_ucd_devices()

        selected = self._pick_ucd_device(ucd_list, app.ucd_list_var.get())
        app.ucd_list_var.set(selected or "")
        app.ucd_list_combo.config(values=ucd_list)

        if app.ca_com_var.get() not in com_ports:
            app.ca_com_var.set(
                com_ports[1] if len(com_ports) > 1 else (com_ports[0] if com_ports else "")
            )
        app.ca_com_combo.config(values=com_ports)

        app.refresh_connection_indicators()

        app.update_config()

    # -- 内部 ----------------------------------------------------

    def _enable_widgets(self) -> None:
        self._set_connect_widgets_state("normal")

    def _set_connect_widgets_state(self, state: str) -> None:
        for attr in (
            "check_button",
            "ucd_connect_button",
            "ca_connect_button",
            "refresh_button",
        ):
            widget = getattr(self._app, attr, None)
            if widget is not None:
                try:
                    widget.configure(state=state)
                except Exception:  # noqa: BLE001
                    pass

    def _log(self, msg: str, *, level: str = "info") -> None:
        log_gui = getattr(self._app, "log_gui", None)
        if log_gui is not None:
            log_gui.log(msg, level=level)


# ─── 旧名字兼容层 ────────────────────────────────────────────────
# pqAutomationApp 类体仍以同名属性挂接这些函数；它们都委托给
# ``self.connection``。Phase 3 完成后这些 shim 可以连同类体的属性
# 挂接一并删除，让 GUI 直接调用 ``self.connection.xxx``。


def get_available_ucd_ports(self: "PQAutomationApp"):
    return self.connection.list_ucd_devices()


def get_available_com_ports(self: "PQAutomationApp"):
    return self.connection.list_com_ports()


def refresh_com_ports(self: "PQAutomationApp"):
    self.connection.refresh_ports()


def check_com_connections(self: "PQAutomationApp"):
    self.connection.check_all_async()


def check_ucd_connection(self: "PQAutomationApp"):
    self.connection.check_ucd_async()


def check_ca_connection(self: "PQAutomationApp"):
    self.connection.check_ca_async()


def update_connection_indicator(self: "PQAutomationApp", indicator, connected):
    _draw_connection_indicator(indicator, "green" if connected else "red")


def refresh_connection_indicators(self: "PQAutomationApp"):
    """根据当前设备状态重画 UCD / CA 指示灯。"""
    if hasattr(self, "ucd_status_indicator"):
        ucd_connected = self.signal_service.is_connected
        _draw_connection_indicator(
            self.ucd_status_indicator,
            "green" if ucd_connected else "gray",
        )
    if hasattr(self, "ca_status_indicator"):
        ca_connected = getattr(self, "ca", None) is not None
        _draw_connection_indicator(
            self.ca_status_indicator,
            "green" if ca_connected else "gray",
        )


def _draw_connection_indicator(canvas, state: str) -> None:
    palette = get_theme_palette()
    color_map = {
        "green": "#2ECC71",
        "red": "#E74C3C",
        "gray": "#9AA3AD",
    }
    fill = color_map.get(state, state)
    border = palette["border"]
    bg = palette["card_bg"]
    try:
        canvas.configure(bg=bg, highlightbackground=border, highlightcolor=border)
        canvas.delete("all")
        # 保持原有视觉：方形状态灯（红/绿/灰）
        canvas.create_rectangle(0, 0, 15, 15, fill=fill, outline=border, width=1)
    except Exception:
        try:
            canvas.config(bg=fill)
        except Exception:
            pass


def check_port_connection(self: "PQAutomationApp", is_ucd=True):
    """[已弃用] 旗参数反模式；保留仅为兼容旧调用点。"""
    if is_ucd:
        return self.connection.connect_ucd(self.ucd_list_var.get())
    return self.connection.connect_ca()


def enable_com_widgets(self: "PQAutomationApp"):
    self.connection._enable_widgets()


def disconnect_com_connections(self: "PQAutomationApp"):
    self.connection.disconnect_all()


def disconnect_ucd_connection(self: "PQAutomationApp"):
    self.connection.disconnect_ucd_only()


def disconnect_ca_connection(self: "PQAutomationApp"):
    self.connection.disconnect_ca_only()


def _get_ca_measure_lock(self: "PQAutomationApp"):
    lock = getattr(self, "_ca_measure_lock", None)
    if lock is None:
        lock = threading.RLock()
        self._ca_measure_lock = lock
    return lock


def _read_ca_display(self: "PQAutomationApp", mode: int):
    """在锁内切换 CA410 Display 模式并立即读取，避免模式串扰。"""
    if getattr(self, "ca", None) is None:
        raise RuntimeError("请先连接 CA410 色度计")

    with _get_ca_measure_lock(self):
        self.ca.set_Display(mode)
        return self.ca.readAllDisplay()


def read_ca_xyLv(self: "PQAutomationApp"):
    """读取 xy/Lv/XYZ（Display 0）。"""
    return _read_ca_display(self, 0)


def read_ca_tcp_duv(self: "PQAutomationApp"):
    """读取 Tcp/duv/Lv/XYZ（Display 1）。"""
    return _read_ca_display(self, 1)


def read_ca_uvLv(self: "PQAutomationApp"):
    """读取 u'/v'/Lv/XYZ（Display 5）。"""
    return _read_ca_display(self, 5)


def read_ca_xyz(self: "PQAutomationApp"):
    """读取 XYZ（Display 7）。"""
    return _read_ca_display(self, 7)


def read_ca_lambda_pe(self: "PQAutomationApp"):
    """读取 λd/Pe/Lv/XYZ（Display 8）。"""
    return _read_ca_display(self, 8)


__all__ = [
    "ConnectionController",
    # 兼容层
    "get_available_ucd_ports",
    "get_available_com_ports",
    "refresh_com_ports",
    "check_com_connections",
    "check_ucd_connection",
    "check_ca_connection",
    "update_connection_indicator",
    "refresh_connection_indicators",
    "check_port_connection",
    "enable_com_widgets",
    "disconnect_com_connections",
    "disconnect_ucd_connection",
    "disconnect_ca_connection",
]


class DeviceConnectionMixin:
    """由 tools/refactor_to_mixins.py 自动生成。
    把本模块的自由函数挂到 PQAutomationApp 上，便于 F12 跳转与类型推断。
    """
    get_available_ucd_ports = get_available_ucd_ports
    get_available_com_ports = get_available_com_ports
    refresh_com_ports = refresh_com_ports
    check_com_connections = check_com_connections
    check_ucd_connection = check_ucd_connection
    check_ca_connection = check_ca_connection
    update_connection_indicator = update_connection_indicator
    refresh_connection_indicators = refresh_connection_indicators
    check_port_connection = check_port_connection
    enable_com_widgets = enable_com_widgets
    disconnect_com_connections = disconnect_com_connections
    disconnect_ucd_connection = disconnect_ucd_connection
    disconnect_ca_connection = disconnect_ca_connection
    _get_ca_measure_lock = _get_ca_measure_lock
    _read_ca_display = _read_ca_display
    read_ca_xyLv = read_ca_xyLv
    read_ca_tcp_duv = read_ca_tcp_duv
    read_ca_uvLv = read_ca_uvLv
    read_ca_xyz = read_ca_xyz
    read_ca_lambda_pe = read_ca_lambda_pe
