#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：base_simulator.py
模块描述：模拟设备基类，定义通用接口

作者：czj
创建日期：2026-07-29
最后修改：2026-07-29
"""

import json
import logging
import threading
import time
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Optional, List

from app.services.mqtt_manager import MqttManager

logger = logging.getLogger(__name__)


class BaseSimulator(ABC):
    """
    模拟设备基类

    所有模拟设备类型（门口屏、无纸化、中控、集控客户端）继承此类。
    提供设备注册、定时上报、指令响应等通用功能。

    Attributes:
        device_id (str): 设备唯一标识
        env_config_id (str): 关联环境配置 ID
        device_type (str): 设备类型标识
        mqtt (MqttManager): MQTT 连接管理器
        _running (bool): 是否正在运行
        _thread (threading.Thread): 定时上报线程
        _report_config (dict): 上报配置，含 interval(秒)/enabled(开关)
        _on_report_callback (callable): 上报回调（用于记录日志）
    """

    def __init__(self, device_id: str, env_config_id: str,
                 device_type: str, mqtt_manager: MqttManager,
                 report_config: Optional[dict] = None,
                 topics: Optional[dict] = None,
                 topic_params: Optional[dict] = None,
                 token_api_host: Optional[str] = None):
        """
        初始化模拟设备

        Args:
            device_id: 设备 ID
            env_config_id: 环境配置 ID
            device_type: 设备类型
            mqtt_manager: MQTT 管理器实例
            report_config: 上报配置，如 {'interval': 30, 'enabled': True}
            topics: 多主题前缀映射，key 为设备类型，value 为主题前缀
            topic_params: 主题动态参数，如 {'room_id': 'A101', 'company_id': '001'}
            token_api_host: Token 接口主机地址（设备启动时调用 getTokenInfoByToken 用）
        """
        self.device_id = device_id
        self.env_config_id = env_config_id
        self.device_type = device_type
        self.mqtt = mqtt_manager
        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._report_config = report_config or {}
        self._report_interval = self._report_config.get('interval', 30)
        self._report_enabled = self._report_config.get('enabled', True)
        self._on_report_callback = None
        self._lock = threading.Lock()
        self.topics = topics or {}
        self.topic_params = topic_params or {}
        self.token_api_host = token_api_host

        # 解析真实主题模板
        self._resolved_topics: dict = {}
        self._has_real_topics: bool = False
        self._resolve_topics()

    def get_topic_prefix(self) -> str:
        """
        获取设备主题前缀

        优先使用环境配置中的多主题映射（topics），如果没有配置则使用默认值。

        Returns:
            str: 主题前缀，如 "device/door"
        """
        if self.topics and self.device_type in self.topics:
            return self.topics[self.device_type]
        return f"device/{self.device_type}"

    def _resolve_topics(self) -> None:
        """
        解析主题模板，用 topic_params 替换占位符生成实际主题

        如果设备类型有真实主题模板（门口屏/无纸化），则使用真实主题；
        否则标记为无真实主题，使用旧格式 fallback。

        对于中控设备，根据 topic_params.device_type 选择对应的主题模板。
        """
        from app.simulators.topic_templates import (
            has_real_topics,
            resolve_all_topics,
            resolve_topics_by_device_type,
        )

        self._has_real_topics = has_real_topics(self.device_type)
        if self._has_real_topics:
            # 确保topic_params中包含device_id（用于主题模板中的{device_id}占位符）
            params_with_device_id = dict(self.topic_params)
            if 'device_id' not in params_with_device_id:
                params_with_device_id['device_id'] = self.device_id

            # 中控设备：根据 device_type 筛选对应主题
            if self.device_type == "central" and "device_type" in self.topic_params:
                self._resolved_topics = resolve_topics_by_device_type(
                    self.device_type, params_with_device_id
                )
            else:
                self._resolved_topics = resolve_all_topics(self.device_type, params_with_device_id)
            logger.info(f"设备 {self.device_id} 解析主题: {self._resolved_topics}")

    def get_resolved_topic(self, template_key: str) -> Optional[str]:
        """
        获取解析后的主题

        Args:
            template_key: 模板 key，如 "conference", "meeting_message"

        Returns:
            str: 解析后的实际主题，如不存在返回 None
        """
        if template_key in self._resolved_topics:
            return self._resolved_topics[template_key]["topic"]
        return None

    def get_publish_topics(self) -> List[dict]:
        """
        获取所有上报主题

        Returns:
            List[dict]: 上报主题列表，每项包含 key, topic, label
        """
        if not self._has_real_topics:
            return []
        return [
            {"key": k, "topic": v["topic"], "label": v["label"]}
            for k, v in self._resolved_topics.items()
            if v["direction"] == "publish"
        ]

    def get_subscribe_topics(self) -> List[dict]:
        """
        获取所有订阅主题

        Returns:
            List[dict]: 订阅主题列表，每项包含 key, topic, label
        """
        if not self._has_real_topics:
            return []
        return [
            {"key": k, "topic": v["topic"], "label": v["label"]}
            for k, v in self._resolved_topics.items()
            if v["direction"] == "subscribe"
        ]

    @abstractmethod
    def build_register_payload(self) -> dict:
        """
        构建设备注册消息

        Returns:
            dict: 注册消息内容
        """
        pass

    @abstractmethod
    def build_status_payload(self) -> dict:
        """
        构建设备状态上报消息

        Returns:
            dict: 状态消息内容
        """
        pass

    def build_reboot_response_payload(self) -> dict:
        """
        构建 rebootResponseTopic 消息体

        包含设备所有固定/模拟字段，符合 Mqtt_Send.py 的消息格式。

        Returns:
            dict: reboot 响应消息内容
        """
        import random
        import json
        from datetime import datetime

        # 从 topic_params 获取动态参数
        company_id = self.topic_params.get("company_id", "001")
        room_id = self.topic_params.get("room_id", "A101")
        app_token = self.topic_params.get("app_token", "ANP-0R5-0013")
        conference_id = self.topic_params.get("conference_id", "")
        conference_name = self.topic_params.get("conference_name", "")

        # 系统时间
        sys_date_obj = datetime.now()
        sys_date = sys_date_obj.strftime("%a %b %d %H:%M:%S GMT+08:00 %Y")
        sys_date_str = sys_date_obj.strftime("%Y-%m-%d %H:%M:%S")

        # 运行时长（模拟）
        seed = hash(self.device_id) % 86400
        hours = seed // 3600
        minutes = (seed % 3600) // 60
        seconds = seed % 60
        sys_elapsed_realtime = f"{hours}:{minutes:02d}:{seconds:02d}"

        # 判断设备类型
        is_paperless = self.device_type == "paperless"

        # 无纸化设备：新格式
        if is_paperless:
            total_mem = 3781
            free_mem = random.randint(1200, 1800)
            internal_total = 53768880128
            internal_free = random.randint(28000000000, 35000000000)

            result_data = {
                "result": {
                    "buildConfig": {
                        "FLAVOR": "normalPaperless",
                        "FLAVOR_FEATURE": "paperless",
                        "FLAVOR_APP_TYPE": "normal",
                        "BUILD_TYPE": "release",
                        "LIB_UB_GVIEW_VERSION_NAME": "0.3.414",
                        "LIB_UB_GVIEW_VERSION_CODE": 414,
                        "LIB_UB_GVIEW_BUILD_DATE": "20260805100311",
                    },
                    "faceVersion": "",
                    "wgtVersion": "0.0.32",
                    "deviceModel": "BAH3-W59",
                    "abiList": ["arm64-v8a", "armeabi-v7a", "armeabi"],
                    "androidId": self.device_id,
                    "appName": "无纸化2.0",
                    "appPackageName": "com.ubains.local.gviewer",
                    "appVersion": 414,
                    "appVersionName": "1.3.414",
                    "battery": round(random.uniform(0.3, 1.0), 1),
                    "bluetoothEnabled": False,
                    "camerasCount": 2,
                    "charging": True,
                    "density": 2,
                    "freeAndTotalMemory": f"{free_mem}M/{total_mem}M",
                    "internalAvailableSize": internal_free,
                    "internalTotalSize": internal_total,
                    "ipAddress": ".".join(str(random.randint(1, 255)) for _ in range(4)),
                    "macAddress": f"{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}",
                    "networkType": "NETWORK_WIFI",
                    "processCpuUsage": "ERR",
                    "resolution": "2000x1200",
                    "romName": "huawei",
                    "rooted": False,
                    "sdkVersionCode": 29,
                    "sdkVersionName": "10",
                    "sysDate": sys_date,
                    "sysDateStr": sys_date_str,
                    "sysElapsedRealtime": sys_elapsed_realtime,
                    "sysLanguage": "zh",
                    "sysSupportedSensors": [
                        "accelerometer-kx021", "mag-mmc5603", "orientation",
                        "als-tcs3707", "gravity", "linear Acceleration",
                        "rotation Vector", "HALL sensor", "uncalibrated Magnetic Field",
                        "significant Motion", "geomagnetic Rotation Vector",
                        "phonecall sensor"
                    ],
                    "authCode": app_token,
                    "conferenceName": conference_name
                }
            }
        else:
            # 门口屏设备：原格式
            total_mem = 1957
            free_mem = random.randint(600, 900)
            internal_total = 11075297280
            internal_free = random.randint(9700000000, 11000000000)

            build_info = {
                "appToken": app_token,
                "companyNumber": company_id,
                "cnum": room_id,
                "conferenceName": conference_name,
                "conferenceId": int(conference_id) if conference_id else 0,
                "defaultQrCodeUrl": None,
                "aliasName": conference_name,
                "serverBaseUrl": "https://192.168.9.86/exapi/",
                "localBindTime": sys_date_str,
                "generalField": json.dumps({
                    "floorPlanPath": "",
                    "conferencePhone": "",
                    "chooseTimeType": 1,
                    "meetingTopicSwitch": "1",
                    "meetingContentSwitch": "1",
                    "meetingReverseTypeSwitch": "1",
                    "seatArrangeSwitch": "0",
                    "meetingVoteSwitch": "1",
                    "jumpToPaperless": 2,
                    "meetingPublishScreenSwitch": "1",
                    "isLeaderJoin": False,
                    "approvalList": []
                })
            }

            result_data = {
                "result": {
                    "buildInfo": build_info,
                    "faceVersion": "4.2.12021020201.1",
                    "wgtVersion": "0.2.134",
                    "deviceModel": "YT-10",
                    "abiList": ["arm64-v8a", "armeabi-v7a", "armeabi"],
                    "androidId": self.device_id,
                    "appName": "UBAINS",
                    "appPackageName": "com.ubains.local.gviewer",
                    "appVersion": 351,
                    "appVersionName": "1.3.351",
                    "battery": round(random.uniform(0.3, 1.0), 1),
                    "bluetoothEnabled": False,
                    "camerasCount": 1,
                    "charging": True,
                    "density": 1,
                    "freeAndTotalMemory": f"{free_mem}M/{total_mem}M",
                    "internalAvailableSize": internal_free,
                    "internalTotalSize": internal_total,
                    "ipAddress": ".".join(str(random.randint(1, 255)) for _ in range(4)),
                    "macAddress": f"{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}",
                    "networkType": "NETWORK_WIFI",
                    "processCpuUsage": f"{random.randint(10, 50)}.{random.randint(0, 99):02d}%",
                    "resolution": "1920x1200",
                    "romName": "yuxian",
                    "rooted": True,
                    "sdkVersionCode": 30,
                    "sdkVersionName": "11",
                    "sysDate": sys_date,
                    "sysDateStr": sys_date_str,
                    "sysElapsedRealtime": sys_elapsed_realtime,
                    "sysLanguage": "en",
                    "sysSupportedSensors": ["Accelerometer sensor"],
                    "buildConfig": {
                        "FLAVOR_APP_TYPE": "rksystem",
                        "BUILD_TYPE": "release",
                        "FLAVOR_FEATURE": "doorscreen",
                        "DEBUG": False,
                        "FLAVOR": "rksystemDoorscreen",
                    },
                    "faceBuildInfo": "arcfacepro4x",
                    "authCode": app_token,
                    "conferenceName": conference_name
                }
            }

        # 外层消息结构
        return {
            "method": "/system/readSystemInfo",
            "clientId": self.device_id,
            "result": json.dumps(result_data, ensure_ascii=False)
        }

    @abstractmethod
    def _on_command(self, topic: str, payload: dict) -> None:
        """
        处理平台下发的指令

        Args:
            topic: 指令主题
            payload: 指令内容
        """
        pass

    def set_report_callback(self, callback) -> None:
        """
        设置上报回调

        Args:
            callback: 回调函数 (topic, payload, direction, status, error) -> None
        """
        self._on_report_callback = callback

    def _notify_report(self, topic: str, payload: dict,
                       direction: str = "publish",
                       status: str = "success",
                       error: Optional[str] = None) -> None:
        """
        通知上报事件（触发回调记录日志）

        Args:
            topic: 主题
            payload: 内容
            direction: 方向
            status: 状态
            error: 错误信息
        """
        if self._on_report_callback:
            try:
                self._on_report_callback(
                    device_id=self.device_id,
                    topic=topic,
                    payload=payload,
                    direction=direction,
                    status=status,
                    error_message=error
                )
            except Exception as e:
                logger.error(f"上报回调异常: {e}")

    def start(self) -> bool:
        """
        启动模拟设备

        流程：
        1. 发送注册消息
        2. 订阅平台下发指令主题
        3. 开启定时上报线程

        如果设备类型有真实主题模板（门口屏/无纸化），使用真实主题；
        否则使用旧格式 {prefix}/{device_type}/{device_id}/{action}。

        Returns:
            bool: 是否启动成功
        """
        with self._lock:
            if self._running:
                logger.warning(f"设备已在运行: {self.device_id}")
                return False

            try:
                # 1. 发送注册消息
                if self._has_real_topics:
                    # 使用真实主题：注册消息发送到第一个 publish 主题
                    publish_topics = self.get_publish_topics()
                    if publish_topics:
                        register_topic_info = publish_topics[0]
                        register_topic = register_topic_info["topic"]
                        topic_key = register_topic_info.get("key", "")
                        # 根据主题 key 构建不同的注册消息体
                        if topic_key == "reboot_response":
                            register_payload = self.build_reboot_response_payload()
                        elif topic_key == "android_broadcast":
                            # 安卓广播主题：心跳上报
                            register_payload = {
                                "type": "heartbeat",
                                "clientId": self.device_id,
                                "appId": "com.ubains.local.gviewer",
                                "deviceId": self.device_id,
                            }
                        else:
                            register_payload = self.build_register_payload()
                    else:
                        register_topic = self.get_topic_prefix()
                        register_payload = self.build_register_payload()
                else:
                    # 旧格式
                    register_topic = f"{self.get_topic_prefix()}/{self.device_id}/register"
                    register_payload = self.build_register_payload()
                reg_success = self.mqtt.publish(self.env_config_id, register_topic, register_payload)
                self._notify_report(register_topic, register_payload,
                                    status="success" if reg_success else "failed")

                # 2. 订阅平台下发指令主题
                if self._has_real_topics:
                    # 使用真实主题：订阅所有 subscribe 方向的主题
                    for sub_info in self.get_subscribe_topics():
                        sub_success = self.mqtt.subscribe(self.env_config_id, sub_info["topic"], self._on_command)
                        if not sub_success:
                            logger.warning(f"订阅指令主题失败: {sub_info['topic']}")
                        else:
                            logger.info(f"订阅指令主题成功: {sub_info['topic']} ({sub_info['label']})")
                else:
                    # 旧格式
                    command_topic = f"{self.get_topic_prefix()}/{self.device_id}/command"
                    sub_success = self.mqtt.subscribe(self.env_config_id, command_topic, self._on_command)
                    if not sub_success:
                        logger.warning(f"订阅指令主题失败: {command_topic}")

                # 3. 发送初始状态
                if self._has_real_topics:
                    # 使用真实主题：状态消息发送到第一个 publish 主题
                    if publish_topics:
                        status_topic = publish_topics[0]["topic"]
                    else:
                        status_topic = self.get_topic_prefix()
                else:
                    status_topic = f"{self.get_topic_prefix()}/{self.device_id}/status"

                status_payload = self.build_status_payload()
                status_payload["status"] = "online"
                self.mqtt.publish(self.env_config_id, status_topic, status_payload)

                # 4. 开启定时上报线程
                self._running = True
                self._thread = threading.Thread(
                    target=self._auto_report_loop,
                    name=f"sim_{self.device_id[:8]}",
                    daemon=True
                )
                self._thread.start()

                logger.info(f"模拟设备启动成功: {self.device_id} ({self.device_type})")
                return True

            except Exception as e:
                self._running = False
                logger.error(f"模拟设备启动失败: {self.device_id}, error={e}")
                return False

    def stop(self) -> bool:
        """
        停止模拟设备

        流程：
        1. 发送离线状态
        2. 停止定时上报线程
        3. 取消订阅指令主题

        Returns:
            bool: 是否停止成功
        """
        with self._lock:
            if not self._running:
                return False

            try:
                self._running = False

                # 发送离线状态
                if self._has_real_topics:
                    publish_topics = self.get_publish_topics()
                    offline_topic = publish_topics[0]["topic"] if publish_topics else self.get_topic_prefix()
                else:
                    offline_topic = f"{self.get_topic_prefix()}/{self.device_id}/status"

                offline_payload = {"status": "offline", "device_id": self.device_id}
                self.mqtt.publish(self.env_config_id, offline_topic, offline_payload)
                self._notify_report(offline_topic, offline_payload, status="success")

                # 取消订阅指令主题
                if self._has_real_topics:
                    for sub_info in self.get_subscribe_topics():
                        self.mqtt.unsubscribe(self.env_config_id, sub_info["topic"])
                else:
                    command_topic = f"{self.get_topic_prefix()}/{self.device_id}/command"
                    self.mqtt.unsubscribe(self.env_config_id, command_topic)

                logger.info(f"模拟设备停止成功: {self.device_id}")
                return True

            except Exception as e:
                logger.error(f"模拟设备停止失败: {self.device_id}, error={e}")
                return False

    def report(self, topic: str, payload: dict) -> bool:
        """
        手动触发上报

        Args:
            topic: 上报主题（相对路径，如 "event" 会补全为完整主题）
            payload: 上报数据

        Returns:
            bool: 是否上报成功
        """
        if not topic.startswith(self.get_topic_prefix()):
            full_topic = f"{self.get_topic_prefix()}/{self.device_id}/{topic}"
        else:
            full_topic = topic

        success = self.mqtt.publish(self.env_config_id, full_topic, payload)
        self._notify_report(full_topic, payload, status="success" if success else "failed")
        return success

    def is_running(self) -> bool:
        """
        检查设备是否在运行

        Returns:
            bool: 是否运行中
        """
        return self._running

    def _auto_report_loop(self) -> None:
        """
        定时上报循环

        在独立线程中运行，定时上报设备状态。
        如果有真实主题模板，轮流使用各 publish 主题上报；
        否则使用旧格式。
        """
        # 检查是否启用定时上报
        if not self._report_enabled:
            logger.info(f"定时上报已禁用: {self.device_id}")
            return

        logger.info(f"定时上报线程启动: {self.device_id}, 间隔={self._report_interval}s")

        # 轮流使用各 publish 主题的索引
        publish_index = 0

        while self._running:
            try:
                status_payload = None
                if self._has_real_topics:
                    # 使用真实主题：轮流使用各 publish 主题上报
                    publish_topics = self.get_publish_topics()
                    if publish_topics:
                        topic_info = publish_topics[publish_index % len(publish_topics)]
                        status_topic = topic_info["topic"]
                        topic_key = topic_info.get("key", "")
                        publish_index += 1
                        # 根据主题 key 构建不同的消息体
                        if topic_key == "reboot_response":
                            status_payload = self.build_reboot_response_payload()
                        elif topic_key == "android_broadcast":
                            # 安卓广播主题：心跳上报
                            # 优先使用子类的 build_heartbeat_payload 方法
                            if hasattr(self, 'build_heartbeat_payload'):
                                status_payload = self.build_heartbeat_payload()
                            else:
                                status_payload = {
                                    "type": "heartbeat",
                                    "clientId": self.device_id,
                                    "appId": "com.ubains.local.gviewer",
                                    "deviceId": self.device_id,
                                }
                        else:
                            status_payload = self.build_status_payload()
                    else:
                        status_topic = self.get_topic_prefix()
                        status_payload = self.build_status_payload()
                else:
                    # 旧格式
                    status_topic = f"{self.get_topic_prefix()}/{self.device_id}/status"
                    status_payload = self.build_status_payload()

                success = self.mqtt.publish(self.env_config_id, status_topic, status_payload)
                self._notify_report(status_topic, status_payload,
                                    status="success" if success else "failed")
            except Exception as e:
                logger.error(f"定时上报异常: {self.device_id}, error={e}")

            # 等待下次上报（分段等待，支持快速停止）
            for _ in range(self._report_interval):
                if not self._running:
                    break
                time.sleep(1)

        logger.info(f"定时上报线程结束: {self.device_id}")