#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：device_sim.py
模块描述：设备模拟模块 API 路由

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

import logging
import json
from datetime import datetime
from typing import Optional, List

from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession

from app.database import get_db
from app.schemas.device_sim import (
    EnvConfigCreate, EnvConfigUpdate, EnvConfigResponse, EnvConfigListResponse,
    SimulatorCreate, SimulatorUpdate, SimulatorResponse, SimulatorListResponse,
    ReportLogResponse, ReportLogListResponse,
    ManualReportRequest, TestConnectionResponse,
    TopicTemplateResponse, TopicTemplateListResponse,
    BatchOperationRequest, BatchOperationResponse,
    DeviceImportResponse,
)
from app.services.device_sim_service import DeviceSimService

logger = logging.getLogger(__name__)

# 创建路由实例
router = APIRouter()


def get_device_sim_service(db: AsyncSession = Depends(get_db)) -> DeviceSimService:
    """
    获取设备模拟服务实例（依赖注入）

    Args:
        db: 数据库会话

    Returns:
        DeviceSimService: 设备模拟服务实例
    """
    return DeviceSimService(db)


# ==================== 主题模板 API ====================


@router.get("/topic-templates/{device_type}", response_model=TopicTemplateListResponse)
async def get_topic_templates(device_type: str):
    """
    获取设备类型的主题模板列表

    返回该设备类型的所有主题模板，包含参数定义和默认值。
    """
    from app.simulators.topic_templates import (
        get_topic_templates as _get_topic_templates,
        has_real_topics,
    )

    templates = _get_topic_templates(device_type)
    return TopicTemplateListResponse(
        device_type=device_type,
        templates=[
            TopicTemplateResponse(
                key=t["key"],
                template=t.get("template", t.get("template_publish", "")),
                label=t["label"],
                params=t["params"],
                param_labels=t["param_labels"],
                param_defaults=t["param_defaults"],
                direction=t.get("direction", "publish"),
                device_type=t.get("device_type"),
            )
            for t in templates
        ],
        has_real_topics=has_real_topics(device_type),
    )


# ==================== 环境配置 API ====================


@router.get("/env-configs", response_model=EnvConfigListResponse)
async def list_env_configs(
    skip: int = Query(0, ge=0, description="跳过的记录数"),
    limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    获取环境配置列表

    支持分页查询，按创建时间倒序返回。
    """
    try:
        items, total = await service.list_env_configs(skip=skip, limit=limit)
        return EnvConfigListResponse(
            items=[EnvConfigResponse.model_validate(item) for item in items],
            total=total
        )
    except Exception as e:
        logger.error(f"获取环境配置列表失败: {e}")
        raise HTTPException(status_code=500, detail=f"获取环境配置列表失败: {str(e)}")


@router.post("/env-configs", response_model=EnvConfigResponse, status_code=201)
async def create_env_config(
    data: EnvConfigCreate,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    新增环境配置

    创建 MQTT Broker 连接配置。
    """
    try:
        config = await service.create_env_config(data)
        return EnvConfigResponse.model_validate(config)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"创建环境配置失败: {e}")
        raise HTTPException(status_code=500, detail=f"创建环境配置失败: {str(e)}")


@router.get("/env-configs/{config_id}", response_model=EnvConfigResponse)
async def get_env_config(
    config_id: str,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    获取环境配置详情
    """
    config = await service.get_env_config(config_id)
    if not config:
        raise HTTPException(status_code=404, detail="环境配置不存在")
    return EnvConfigResponse.model_validate(config)


@router.put("/env-configs/{config_id}", response_model=EnvConfigResponse)
async def update_env_config(
    config_id: str,
    data: EnvConfigUpdate,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    更新环境配置
    """
    config = await service.update_env_config(config_id, data)
    if not config:
        raise HTTPException(status_code=404, detail="环境配置不存在")
    return EnvConfigResponse.model_validate(config)


@router.delete("/env-configs/{config_id}")
async def delete_env_config(
    config_id: str,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    删除环境配置

    有关联模拟设备时禁止删除。
    """
    try:
        success = await service.delete_env_config(config_id)
        if not success:
            raise HTTPException(status_code=404, detail="环境配置不存在")
        return {"message": "环境配置已删除", "id": config_id}
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"删除环境配置失败: {e}")
        raise HTTPException(status_code=500, detail=f"删除环境配置失败: {str(e)}")


@router.post("/env-configs/{config_id}/test", response_model=TestConnectionResponse)
async def test_env_connection(
    config_id: str,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    测试 MQTT 连接

    测试指定环境配置的 MQTT Broker 连接是否正常。
    """
    try:
        result = await service.test_env_connection(config_id)
        return result
    except Exception as e:
        logger.error(f"测试连接失败: {e}")
        raise HTTPException(status_code=500, detail=f"测试连接失败: {str(e)}")


# ==================== 模拟设备 API ====================


@router.get("/devices", response_model=SimulatorListResponse)
async def list_simulators(
    skip: int = Query(0, ge=0, description="跳过的记录数"),
    limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
    device_type: Optional[str] = Query(None, description="设备类型筛选"),
    env_config_id: Optional[str] = Query(None, description="环境配置筛选"),
    status: Optional[str] = Query(None, description="状态筛选"),
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    获取模拟设备列表

    支持按设备类型、环境配置、状态筛选。
    """
    try:
        items, total = await service.list_simulators(
            skip=skip, limit=limit,
            device_type=device_type,
            env_config_id=env_config_id,
            status=status,
        )
        return SimulatorListResponse(
            items=[SimulatorResponse.model_validate(item) for item in items],
            total=total
        )
    except Exception as e:
        logger.error(f"获取模拟设备列表失败: {e}")
        raise HTTPException(status_code=500, detail=f"获取模拟设备列表失败: {str(e)}")


@router.post("/devices", response_model=SimulatorResponse, status_code=201)
async def create_simulator(
    data: SimulatorCreate,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    创建模拟设备

    支持 5 种设备类型：door(门口屏)、paperless(无纸化)、central(中控)、client(集控客户端)
    """
    try:
        device = await service.create_simulator(data)
        return SimulatorResponse.model_validate(device)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"创建模拟设备失败: {e}")
        raise HTTPException(status_code=500, detail=f"创建模拟设备失败: {str(e)}")


# ==================== 批量操作 API ====================
# 注意：批量操作路由必须放在 /devices/{device_id} 之前，避免 batch 被当作 device_id


@router.get("/devices/import/template")
async def download_import_template(
    device_type: str = Query(..., pattern="^(door|paperless|central|client)$", description="设备类型"),
):
    """
    下载设备导入模板

    根据设备类型生成对应的 Excel 模板文件。
    """
    from openpyxl import Workbook
    from fastapi.responses import StreamingResponse
    from io import BytesIO

    wb = Workbook()
    ws = wb.active
    ws.title = "设备导入"

    # 表头
    headers = ["环境配置名称", "设备名称", "设备编号"]
    if device_type == "door":
        headers.append("授权码(app_token)")
    elif device_type == "central":
        headers.append("会议室编号")
        headers.append("主题类型")
    headers.extend(["自动重连", "启用定时上报", "上报间隔(秒)"])
    ws.append(headers)

    # 示例行
    example = ["测试环境", f"{device_type}_001", f"{device_type}_device_001"]
    if device_type == "door":
        example.append("AUTH-0001")
    elif device_type == "central":
        example.append("A101")
        example.append("设备在线")
    example.extend(["是", "是", "30"])
    ws.append(example)

    # 列宽
    for idx, header in enumerate(headers, 1):
        col_letter = chr(64 + idx) if idx <= 26 else None
        if col_letter:
            ws.column_dimensions[col_letter].width = max(len(header) * 2, 15)

    # 保存到缓冲区
    buffer = BytesIO()
    wb.save(buffer)
    buffer.seek(0)

    from urllib.parse import quote
    device_type_label = {"door": "门口屏", "paperless": "无纸化", "central": "中控", "client": "集控客户端"}
    filename = f"{device_type_label.get(device_type, device_type)}_设备导入模板.xlsx"
    encoded_filename = quote(filename)

    return StreamingResponse(
        buffer,
        media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"},
    )


@router.post("/devices/import", response_model=DeviceImportResponse)
async def import_devices(
    file: UploadFile = File(..., description="Excel 文件 (.xlsx)"),
    device_type: str = Query(..., pattern="^(door|paperless|central|client)$", description="设备类型"),
    service: DeviceSimService = Depends(get_device_sim_service),
):
    """
    批量导入模拟设备

    上传 Excel 文件批量创建设备。Excel 列：环境配置名称、设备名称、设备编号、
    授权码(可选，门口屏)、会议室编号(可选，中控)、自动重连(可选)、启用定时上报(可选)、上报间隔(可选)。
    device_type 由前端页面决定，不包含在 Excel 中。
    """
    # 校验文件类型
    if not file.filename or not file.filename.endswith('.xlsx'):
        raise HTTPException(status_code=400, detail="仅支持 .xlsx 格式的 Excel 文件")

    # 读取文件内容
    try:
        file_bytes = await file.read()
        if len(file_bytes) == 0:
            raise HTTPException(status_code=400, detail="文件为空")
        if len(file_bytes) > 10 * 1024 * 1024:  # 10MB 限制
            raise HTTPException(status_code=400, detail="文件大小不能超过 10MB")
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"读取文件失败: {str(e)}")

    # 执行导入
    try:
        result = await service.import_simulators_from_excel(file_bytes, device_type)
        return DeviceImportResponse(**result)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"批量导入设备失败: {e}")
        raise HTTPException(status_code=500, detail=f"批量导入设备失败: {str(e)}")


@router.post("/devices/batch/start", response_model=BatchOperationResponse)
async def batch_start_simulators(
    data: BatchOperationRequest,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    批量启动模拟设备

    批量启动多个设备，返回成功/失败数量。
    """
    try:
        result = await service.batch_start_simulators(data.device_ids)
        return BatchOperationResponse(**result)
    except Exception as e:
        logger.error(f"批量启动失败: {e}")
        raise HTTPException(status_code=500, detail=f"批量启动失败: {str(e)}")


@router.post("/devices/batch/stop", response_model=BatchOperationResponse)
async def batch_stop_simulators(
    data: BatchOperationRequest,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    批量停止模拟设备

    批量停止多个设备，返回成功/失败数量。
    """
    try:
        result = await service.batch_stop_simulators(data.device_ids)
        return BatchOperationResponse(**result)
    except Exception as e:
        logger.error(f"批量停止失败: {e}")
        raise HTTPException(status_code=500, detail=f"批量停止失败: {str(e)}")


@router.delete("/devices/batch", response_model=BatchOperationResponse)
async def batch_delete_simulators(
    data: BatchOperationRequest,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    批量删除模拟设备

    批量删除多个设备，返回成功/失败数量。
    """
    try:
        result = await service.batch_delete_simulators(data.device_ids)
        return BatchOperationResponse(**result)
    except Exception as e:
        logger.error(f"批量删除失败: {e}")
        raise HTTPException(status_code=500, detail=f"批量删除失败: {str(e)}")


# ==================== 单设备操作 API ====================


@router.get("/devices/{device_id}", response_model=SimulatorResponse)
async def get_simulator(
    device_id: str,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    获取模拟设备详情
    """
    device = await service.get_simulator(device_id)
    if not device:
        raise HTTPException(status_code=404, detail="模拟设备不存在")
    return SimulatorResponse.model_validate(device)


@router.put("/devices/{device_id}", response_model=SimulatorResponse)
async def update_simulator(
    device_id: str,
    data: SimulatorUpdate,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    更新模拟设备配置
    """
    device = await service.update_simulator(device_id, data)
    if not device:
        raise HTTPException(status_code=404, detail="模拟设备不存在")
    return SimulatorResponse.model_validate(device)


@router.patch("/devices/{device_id}/type")
async def switch_device_type(
    device_id: str,
    device_type_name: str = Query(..., description="设备类型标识（room_online/device_online/audio/video/control/network/power）"),
    service: DeviceSimService = Depends(get_device_sim_service),
):
    """
    切换中控设备上报类型

    支持在运行时切换中控设备的上报主题类型。
    如果设备正在运行，会先停止再重启以应用新配置。
    """
    try:
        result = await service.switch_device_type(device_id, device_type_name)
        return result
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"切换主题类型失败: {e}")
        raise HTTPException(status_code=500, detail=f"切换主题类型失败: {str(e)}")


@router.delete("/devices/{device_id}")
async def delete_simulator(
    device_id: str,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    删除模拟设备

    会先停止设备，再删除关联上报记录。
    """
    try:
        success = await service.delete_simulator(device_id)
        if not success:
            raise HTTPException(status_code=404, detail="模拟设备不存在")
        return {"message": "模拟设备已删除", "id": device_id}
    except Exception as e:
        logger.error(f"删除模拟设备失败: {e}")
        raise HTTPException(status_code=500, detail=f"删除模拟设备失败: {str(e)}")


@router.post("/devices/{device_id}/start")
async def start_simulator(
    device_id: str,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    启动模拟设备

    连接 MQTT Broker，发送注册消息，开始定时上报。
    """
    try:
        success = await service.start_simulator(device_id)
        return {"message": "模拟设备已启动", "id": device_id, "status": "running"}
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except ConnectionError as e:
        raise HTTPException(status_code=502, detail=str(e))
    except Exception as e:
        logger.error(f"启动模拟设备失败: {e}")
        raise HTTPException(status_code=500, detail=f"启动模拟设备失败: {str(e)}")


@router.post("/devices/{device_id}/stop")
async def stop_simulator(
    device_id: str,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    停止模拟设备

    发送离线状态消息，停止定时上报。
    """
    try:
        success = await service.stop_simulator(device_id)
        return {"message": "模拟设备已停止", "id": device_id, "status": "stopped"}
    except Exception as e:
        logger.error(f"停止模拟设备失败: {e}")
        raise HTTPException(status_code=500, detail=f"停止模拟设备失败: {str(e)}")


@router.post("/devices/{device_id}/report")
async def manual_report(
    device_id: str,
    data: ManualReportRequest,
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    手动触发上报

    指定主题和数据内容，手动触发设备上报。
    """
    try:
        success = await service.manual_report(device_id, data.topic, data.payload)
        return {"message": "上报成功", "success": success}
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"手动上报失败: {e}")
        raise HTTPException(status_code=500, detail=f"手动上报失败: {str(e)}")


# ==================== 上报记录 API ====================


@router.get("/report-logs", response_model=ReportLogListResponse)
async def list_report_logs(
    device_id: Optional[str] = Query(None, description="设备 ID 筛选"),
    direction: Optional[str] = Query(None, description="方向筛选：publish(上报)/subscribe(接收)"),
    skip: int = Query(0, ge=0, description="跳过的记录数"),
    limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
    start_time: Optional[datetime] = Query(None, description="开始时间"),
    end_time: Optional[datetime] = Query(None, description="结束时间"),
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    获取上报记录列表

    支持按设备、方向、时间范围筛选和分页。
    """
    try:
        items, total = await service.list_report_logs(
            device_id=device_id,
            direction=direction,
            skip=skip,
            limit=limit,
            start_time=start_time,
            end_time=end_time,
        )
        return ReportLogListResponse(
            items=[ReportLogResponse.model_validate(item) for item in items],
            total=total
        )
    except Exception as e:
        logger.error(f"获取上报记录失败: {e}")
        raise HTTPException(status_code=500, detail=f"获取上报记录失败: {str(e)}")


@router.delete("/report-logs/cleanup")
async def cleanup_report_logs(
    days: int = Query(7, ge=1, le=30, description="保留天数"),
    service: DeviceSimService = Depends(get_device_sim_service)
):
    """
    清理过期上报记录

    默认保留 7 天，可配置保留天数（1-30天）。
    """
    try:
        count = await service.cleanup_report_logs(days=days)
        return {"message": f"已清理 {count} 条过期记录", "cleaned_count": count}
    except Exception as e:
        logger.error(f"清理上报记录失败: {e}")
        raise HTTPException(status_code=500, detail=f"清理上报记录失败: {str(e)}")


# ==================== WebSocket 实时消息推送 ====================

# 设备模拟消息 WebSocket 连接管理
_device_ws_connections: List[WebSocket] = []


async def _broadcast_device_message(message: dict):
    """向所有连接的 WebSocket 客户端广播设备消息"""
    if not _device_ws_connections:
        return

    message_str = json.dumps(message, ensure_ascii=False)
    disconnected = []

    for ws in _device_ws_connections:
        try:
            await ws.send_text(message_str)
        except Exception as e:
            logger.warning(f"WebSocket 发送失败: {e}")
            disconnected.append(ws)

    # 清理断开的连接
    for ws in disconnected:
        if ws in _device_ws_connections:
            _device_ws_connections.remove(ws)


@router.websocket("/ws/messages")
async def websocket_device_messages(websocket: WebSocket):
    """
    设备消息实时推送 WebSocket 端点

    连接后，客户端会实时收到所有设备的上报和下发消息。

    消息格式：
    {
        "event": "device_message",
        "data": {
            "device_id": "sim_xxx",
            "device_name": "门口屏-001",
            "device_type": "door",
            "topic": "rebootResponseTopic",
            "direction": "publish",
            "payload": {...},
            "timestamp": "2026-08-05T14:32:01"
        }
    }
    """
    await websocket.accept()
    _device_ws_connections.append(websocket)
    logger.info(f"WebSocket 连接已建立，当前连接数: {len(_device_ws_connections)}")

    try:
        # 发送连接确认
        await websocket.send_text(json.dumps({
            "event": "connected",
            "message": "已连接到设备消息流"
        }))

        # 保持连接，等待客户端消息或断开
        while True:
            data = await websocket.receive_text()
            # 可以处理客户端发来的订阅过滤等命令
            try:
                msg = json.loads(data)
                if msg.get("type") == "ping":
                    await websocket.send_text(json.dumps({"event": "pong"}))
            except json.JSONDecodeError:
                pass

    except WebSocketDisconnect:
        logger.info("WebSocket 客户端断开连接")
    except Exception as e:
        logger.error(f"WebSocket 异常: {e}")
    finally:
        if websocket in _device_ws_connections:
            _device_ws_connections.remove(websocket)
        logger.info(f"WebSocket 连接已关闭，当前连接数: {len(_device_ws_connections)}")


# 注册消息推送回调到 service 层
def _register_ws_callback():
    """注册 WebSocket 推送回调到 device_sim_service"""
    from app.services.device_sim_service import set_message_callback

    async def _on_device_message(device_id: str, device_name: str, device_type: str,
                                  topic: str, direction: str, payload: dict):
        """设备消息回调"""
        await _broadcast_device_message({
            "event": "device_message",
            "data": {
                "device_id": device_id,
                "device_name": device_name,
                "device_type": device_type,
                "topic": topic,
                "direction": direction,
                "payload": payload,
                "timestamp": datetime.now().isoformat()
            }
        })

    set_message_callback(_on_device_message)


# 在模块加载时注册回调
_register_ws_callback()