# opsky_atr30007.py
import logging
import time as time_mod
import csv
from datetime import datetime
from typing import Optional, Dict, Any

# 兼容 pymodbus 在不同版本中的位置与 API
try:
    from pymodbus.client import ModbusTcpClient
except Exception:
    ModbusTcpClient = None

# 导入 run_raman_test（假定与本文件同目录）
# 如果你的项目是包结构且原先使用相对导入，请改回 `from .raman_module import run_raman_test`
try:
    from .raman_module import run_raman_test
except Exception:
    # 延迟导入失败不会阻止主流程（在 run 时会再尝试）
    run_raman_test = None

logger = logging.getLogger("opsky")
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", "%y-%m-%d %H:%M:%S")
ch.setFormatter(formatter)
logger.addHandler(ch)


class opsky_ATR30007:
    """
    封装 UniLabOS 设备动作逻辑，兼容 pymodbus 2.x / 3.x。
    放在独立文件中：opsky_atr30007.py
    """

    def __init__(
        self,
        plc_ip: str = "192.168.1.88",
        plc_port: int = 502,
        robot_ip: str = "192.168.1.200",
        robot_port: int = 502,
        scan_csv_file: str = "scan_results.csv",
    ):
        self.plc_ip = plc_ip
        self.plc_port = plc_port
        self.robot_ip = robot_ip
        self.robot_port = robot_port
        self.scan_csv_file = scan_csv_file

    # ----------------- 参数字符串转换 helpers -----------------
    @staticmethod
    def _str_to_int(s, default):
        try:
            return int(float(str(s).strip()))
        except Exception:
            return int(default)

    @staticmethod
    def _str_to_float(s, default):
        try:
            return float(str(s).strip())
        except Exception:
            return float(default)

    @staticmethod
    def _str_to_bool(s, default):
        try:
            v = str(s).strip().lower()
            if v in ("true", "1", "yes", "y", "t"):
                return True
            if v in ("false", "0", "no", "n", "f"):
                return False
            return default
        except Exception:
            return default

    # ----------------- Modbus / 安全读写 -----------------
    @staticmethod
    def _adapt_req_kwargs_for_read(func_name: str, args: tuple, kwargs: dict):
        # 如果调用方传的是 (address, count) positional，在新版接口可能是 address=..., count=...
        if len(args) == 2 and func_name.startswith("read_"):
            address, count = args
            args = ()
            kwargs.setdefault("address", address)
            kwargs.setdefault("count", count)
        return args, kwargs

    @staticmethod
    def _adapt_req_kwargs_for_write(func_name: str, args: tuple, kwargs: dict):
        if len(args) == 2 and func_name.startswith("write_"):
            address, value = args
            args = ()
            kwargs.setdefault("address", address)
            kwargs.setdefault("value", value)
        return args, kwargs

    def ensure_connected(self, client, name, ip, port):
        """确保连接存在，失败则尝试重连并返回新的 client 或 None"""
        if client is None:
            return None
        try:
            # 不同 pymodbus 版本可能有不同方法检测 socket
            is_open = False
            try:
                is_open = bool(client.is_socket_open())
            except Exception:
                # fallback: try to read nothing or attempt connection test
                try:
                    # 轻试一次
                    is_open = client.connected if hasattr(client, "connected") else False
                except Exception:
                    is_open = False

            if not is_open:
                logger.warning("%s 掉线，尝试重连...", name)
                try:
                    client.close()
                except Exception:
                    pass
                time_mod.sleep(0.5)
                if ModbusTcpClient:
                    new_client = ModbusTcpClient(ip, port=port)
                    try:
                        if new_client.connect():
                            logger.info("%s 重新连接成功 (%s:%s)", name, ip, port)
                            return new_client
                    except Exception:
                        pass
                logger.warning("%s 重连失败", name)
                time_mod.sleep(1)
                return None
            return client
        except Exception as e:
            logger.exception("%s 连接检查异常: %s", name, e)
            return None

    def safe_read(self, client, name, func, *args, retries=3, delay=0.3, **kwargs):
        """兼容 pymodbus 2.x/3.x 的读函数，返回 response 或 None"""
        if client is None:
            return None
        for attempt in range(1, retries + 1):
            try:
                # adapt args/kwargs for different API styles
                args, kwargs = self._adapt_req_kwargs_for_read(func.__name__, args, kwargs)
                # unit->slave compatibility
                if "unit" in kwargs:
                    kwargs["slave"] = kwargs.pop("unit")
                res = func(*args, **kwargs)
                # pymodbus Response 在不同版本表现不同，尽量检测错误
                if res is None:
                    raise RuntimeError("返回 None")
                if hasattr(res, "isError") and res.isError():
                    raise RuntimeError("Modbus 返回 isError()")
                return res
            except Exception as e:
                logger.warning("%s 读异常 (尝试 %d/%d): %s", name, attempt, retries, e)
            time_mod.sleep(delay)
        logger.error("%s 连续读取失败 %d 次", name, retries)
        return None

    def safe_write(self, client, name, func, *args, retries=3, delay=0.3, **kwargs):
        """兼容 pymodbus 2.x/3.x 的写函数，返回 True/False"""
        if client is None:
            return False
        for attempt in range(1, retries + 1):
            try:
                args, kwargs = self._adapt_req_kwargs_for_write(func.__name__, args, kwargs)
                if "unit" in kwargs:
                    kwargs["slave"] = kwargs.pop("unit")
                res = func(*args, **kwargs)
                if res is None:
                    raise RuntimeError("返回 None")
                if hasattr(res, "isError") and res.isError():
                    raise RuntimeError("Modbus 返回 isError()")
                return True
            except Exception as e:
                logger.warning("%s 写异常 (尝试 %d/%d): %s", name, attempt, retries, e)
            time_mod.sleep(delay)
        logger.error("%s 连续写入失败 %d 次", name, retries)
        return False

    def wait_with_quit_check(self, robot, seconds, addr_quit=270):
        """等待指定时间，同时每 0.2s 检查 R270 是否为 1（立即退出）"""
        if robot is None:
            time_mod.sleep(seconds)
            return False
        checks = max(1, int(seconds / 0.2))
        for _ in range(checks):
            rr = self.safe_read(robot, "机器人", robot.read_holding_registers, address=addr_quit, count=1)
            if rr and getattr(rr, "registers", [None])[0] == 1:
                logger.info("检测到 R270=1，立即退出等待")
                return True
            time_mod.sleep(0.2)
        return False

    # ----------------- 主流程 run_once -----------------
    def run_once(
        self,
        integration_time: str = "5000",
        laser_power: str = "200",
        save_csv: str = "true",
        save_plot: str = "true",
        normalize: str = "true",
        norm_max: str = "1.0",
        **_: Any,
    ) -> Dict[str, Any]:
        result: Dict[str, Any] = {"success": False, "event": "none", "details": {}}

        integration_time_v = self._str_to_int(integration_time, 5000)
        laser_power_v = self._str_to_int(laser_power, 200)
        save_csv_v = self._str_to_bool(save_csv, True)
        save_plot_v = self._str_to_bool(save_plot, True)
        normalize_v = self._str_to_bool(normalize, True)
        norm_max_v = None if norm_max in (None, "", "none", "null") else self._str_to_float(norm_max, 1.0)

        if ModbusTcpClient is None:
            result["details"]["error"] = "未安装 pymodbus，无法执行连接"
            logger.error(result["details"]["error"])
            return result

        # 建立连接
        plc = ModbusTcpClient(self.plc_ip, port=self.plc_port)
        robot = ModbusTcpClient(self.robot_ip, port=self.robot_port)
        try:
            if not plc.connect():
                result["details"]["error"] = "无法连接 PLC"
                logger.error(result["details"]["error"])
                return result
            if not robot.connect():
                plc.close()
                result["details"]["error"] = "无法连接 机器人"
                logger.error(result["details"]["error"])
                return result

            logger.info("✅ PLC 与 机器人连接成功")
            time_mod.sleep(0.2)

            # 伺服使能 (coil 写示例)
            if self.safe_write(plc, "PLC", plc.write_coil, 10, True):
                logger.info("✅ 伺服使能成功 (M10=True)")
            else:
                logger.warning("⚠️ 伺服使能失败")

            # 初始化 CSV 文件
            try:
                with open(self.scan_csv_file, "w", newline="", encoding="utf-8") as f:
                    csv.writer(f).writerow(["Bottle_No", "Scan_Result", "Time"])
            except Exception as e:
                logger.warning("⚠️ 初始化CSV失败: %s", e)

            bottle_count = 0
            logger.info("🟢 等待机器人触发信号... (R260=1扫码 / R256=1拉曼 / R270=1退出)")

            # 主循环：仅响应事件（每次循环后短暂 sleep）
            while True:
                plc = self.ensure_connected(plc, "PLC", self.plc_ip, self.plc_port) or plc
                robot = self.ensure_connected(robot, "机器人", self.robot_ip, self.robot_port) or robot

                # 检查退出寄存器
                quit_signal = self.safe_read(robot, "机器人", robot.read_holding_registers, 270, 1)
                if quit_signal and getattr(quit_signal, "registers", [None])[0] == 1:
                    logger.info("🟥 检测到 R270=1，准备退出...")
                    result["event"] = "quit"
                    result["success"] = True
                    break

                # 读取关键寄存器（256..260）
                rr = self.safe_read(robot, "机器人", robot.read_holding_registers, 256, 5)
                if not rr or not hasattr(rr, "registers"):
                    time_mod.sleep(0.3)
                    continue

                r256, r257, r258, r259, r260 = (rr.registers + [0, 0, 0, 0, 0])[:5]

                # ---------- 扫码逻辑 ----------
                if r260 == 1:
                    bottle_count += 1
                    logger.info("📸 第 %d 瓶触发扫码 (R260=1)", bottle_count)
                    try:
                        # 调用外部扫码函数（用户实现）
                        from .dmqfengzhuang import scan_once as scan_once_local
                        scan_result = scan_once_local(ip="192.168.1.50", port_in=2001, port_out=2002)
                        if scan_result:
                            logger.info("✅ 扫码成功: %s", scan_result)
                            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                            with open(self.scan_csv_file, "a", newline="", encoding="utf-8") as f:
                                csv.writer(f).writerow([bottle_count, scan_result, timestamp])
                        else:
                            logger.warning("⚠️ 扫码失败或无返回")
                    except Exception as e:
                        logger.exception("❌ 扫码异常: %s", e)

                    # 写 R260->0, R261->1
                    self.safe_write(robot, "机器人", robot.write_register, 260, 0)
                    time_mod.sleep(0.15)
                    self.safe_write(robot, "机器人", robot.write_register, 261, 1)
                    logger.info("➡️ 扫码完成 (R260→0, R261→1)")
                    result["event"] = "scan"
                    result["success"] = True

                # ---------- 拉曼逻辑 ----------
                if r256 == 1:
                    logger.info("⚙️ 检测到 R256=1（放瓶完成）")
                    # PLC 电机右转指令
                    self.safe_write(plc, "PLC", plc.write_register, 1199, 1)
                    self.safe_write(plc, "PLC", plc.write_register, 1200, 1)
                    logger.info("➡️ 电机右转中...")
                    if self.wait_with_quit_check(robot, 3):
                        result["event"] = "quit"
                        break
                    self.safe_write(plc, "PLC", plc.write_register, 1199, 0)
                    logger.info("✅ 电机右转完成")

                    # 调用拉曼测试（尽量捕获异常并记录）
                    logger.info("🧪 开始拉曼测试...")
                    try:
                        # 尝试使用模块导入好的 run_raman_test，否则再动态导入
                        rr_func = run_raman_test
                        if rr_func is None:
                            from raman_module import run_raman_test as rr_func
                        success, file_prefix, df = rr_func(
                            integration_time=integration_time_v,
                            laser_power=laser_power_v,
                            save_csv=save_csv_v,
                            save_plot=save_plot_v,
                            normalize=normalize_v,
                            norm_max=norm_max_v,
                        )
                        if success:
                            logger.info("✅ 拉曼测试完成: %s", file_prefix)
                            result["event"] = "raman"
                            result["success"] = True
                        else:
                            logger.warning("⚠️ 拉曼测试失败")
                    except Exception as e:
                        logger.exception("❌ 拉曼模块异常: %s", e)

                    # 电机左转回位
                    self.safe_write(plc, "PLC", plc.write_register, address=1299, value=1)
                    self.safe_write(plc, "PLC", plc.write_register, address=1300, value=1)
                    logger.info("⬅️ 电机左转中...")
                    if self.wait_with_quit_check(robot, 3):
                        result["event"] = "quit"
                        break
                    self.safe_write(plc, "PLC", plc.write_register, address=1299, value=0)
                    logger.info("✅ 电机左转完成")

                    # 通知机器人拉曼完成 R257=1
                    self.safe_write(robot, "机器人", robot.write_register, address=257, value=1)
                    logger.info("✅ 已写入 R257=1（拉曼完成）")

                    # 延迟后清零 R256
                    logger.info("⏳ 延迟4秒后清零 R256")
                    if self.wait_with_quit_check(robot, 4):
                        result["event"] = "quit"
                        break
                    self.safe_write(robot, "机器人", robot.write_register, address=256, value=0)
                    logger.info("✅ 已清零 R256")

                    # 等待机器人清 R257
                    logger.info("等待 R257 清零中...")
                    while True:
                        rr2 = self.safe_read(robot, "机器人", robot.read_holding_registers, address=257, count=1)
                        if rr2 and getattr(rr2, "registers", [None])[0] == 0:
                            logger.info("✅ 检测到 R257=0，准备下一循环")
                            break
                        if self.wait_with_quit_check(robot, 1):
                            result["event"] = "quit"
                            break
                        time_mod.sleep(0.2)

                time_mod.sleep(0.25)

        finally:
            logger.info("🧹 开始清理...")
            try:
                self.safe_write(plc, "PLC", plc.write_coil, address=10, value=False)
            except Exception:
                pass
            for addr in [256, 257, 260, 261, 270]:
                try:
                    self.safe_write(robot, "机器人", robot.write_register, address=addr, value=0)
                except Exception:
                    pass

            try:
                if plc:
                    plc.close()
            except Exception:
                pass
            try:
                if robot:
                    robot.close()
            except Exception:
                pass
            logger.info("🔚 已关闭所有连接")

        return result
