"""
MCP 服务器

包含 3 个工具：
0. get_devices - 列出所有设备
1. get_status - 设备全量状态（合并 10 个 trait 查询）
2. get_map_content - 地图内容（独立重量级请求）
"""

import asyncio
import dataclasses
import functools
import logging
from enum import Enum
from typing import Any

from mcp.server.fastmcp import FastMCP

from .connection import (
    AmbiguousDeviceError,
    AuthExpiredError,
    ConnectionManager,
    DeviceNotFoundError,
    NoDeviceError,
    lifespan,
)

logger = logging.getLogger(__name__)


def _handle_errors(func):
    """包装工具函数，将已知异常转为结构化错误响应"""
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        try:
            return await func(*args, **kwargs)
        except (AuthExpiredError, NoDeviceError, AmbiguousDeviceError,
                DeviceNotFoundError, ValueError) as e:
            return {"error": str(e)}
        except Exception as e:
            logger.exception("工具 %s 执行失败", func.__name__)
            return {"error": f"内部错误: {type(e).__name__}: {e}"}
    return wrapper

# 创建 FastMCP 实例
mcp = FastMCP(
    "roborock-mcp",
    lifespan=lifespan,
)


def get_conn() -> ConnectionManager:
    """从 MCP context 获取 ConnectionManager"""
    return mcp.get_context().request_context.lifespan_context["conn"]


async def _safe_refresh(trait, name: str) -> bool:
    """安全刷新单个 trait，失败不影响其他"""
    try:
        await trait.refresh()
        return True
    except Exception as e:
        logger.warning("Trait %s refresh 失败: %s", name, e)
        return False


def _serialize_value(value: Any) -> Any:
    """将 Enum 等特殊类型转为可序列化值"""
    if value is None:
        return None
    if isinstance(value, Enum):
        return value.name
    if isinstance(value, list):
        return [_serialize_value(v) for v in value]
    return value


# ============================================================================
# 工具定义
# ============================================================================

@mcp.tool(
    annotations={
        "title": "List Roborock Devices",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def get_devices() -> dict[str, Any]:
    """列出账号下所有 Roborock 设备。

    通过 Roborock 云端 API 获取设备列表（不走 MQTT），返回每台设备的基本信息。

    Returns:
        dict: {
            "count": int,           # 设备数量
            "devices": [
                {
                    "name": str,    # 设备名称（用户在 App 中设置的名称）
                    "duid": str,    # 设备唯一标识符
                    "model": str,   # 设备型号（如 "roborock.vacuum.a75"）
                    "firmware": str # 固件版本号
                }
            ]
        }
    """
    conn = get_conn()
    devices = await conn.list_devices()

    result = {
        "count": len(devices),
        "devices": []
    }

    for device in devices:
        info = {
            "name": device.name,
            "duid": device.duid,
            "model": device.product.model if device.product else None,
            "firmware": device.device_info.fv if device.device_info else None,
        }
        result["devices"].append(info)

    return result


@mcp.tool(
    annotations={
        "title": "Get Device Full Status",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def get_status(device_name: str | None = None) -> dict[str, Any]:
    """获取 Roborock 设备全量状态（通过 MQTT 实时查询 10 个 trait）。

    并行查询设备的所有信息并返回扁平化字典，包含：
    - 实时状态：电量、吸力、清洁进度、基座状态、错误码等 60+ 字段
    - 勿扰模式：开关状态及时间段
    - 清洁历史：累计清洁时间/面积/次数，最近一次清洁详情
    - 音量、房间列表（含 segment_id）、地图列表、耗材使用时间
    - 家庭布局、网络信息（IP/SSID/RSSI）、设备特性标志（90+ is_* 布尔值）

    单个 trait 查询失败不影响其他 trait 返回，失败的 trait 对应字段会缺失。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略，自动选择唯一设备。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)

    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    props = device.v1_properties

    # 并行刷新所有 trait
    trait_names = [
        "status", "dnd", "clean_summary", "sound_volume",
        "rooms", "maps", "consumables", "home",
        "network_info", "device_features",
    ]
    refresh_results = await asyncio.gather(
        *[_safe_refresh(getattr(props, name), name) for name in trait_names]
    )
    ok = dict(zip(trait_names, refresh_results))

    result: dict[str, Any] = {"device_name": device.name}

    # --- status trait ---
    if ok["status"]:
        status = props.status

        # 所有 dataclass 字段
        for f in dataclasses.fields(status):
            if f.name.startswith("_"):
                continue
            result[f.name] = _serialize_value(getattr(status, f.name, None))

        # computed properties
        result["state_name"] = status.state_name
        result["error_code_name"] = status.error_code_name
        result["clean_area_m2"] = status.square_meter_clean_area
        result["current_map"] = status.current_map
        result["fan_speed_name"] = status.fan_speed_name
        result["water_mode_name"] = status.water_mode_name
        result["mop_route_name"] = status.mop_route_name

        # dss 拆解的 computed properties
        result["clear_water_box_status"] = _serialize_value(status.clear_water_box_status)
        result["dirty_water_box_status"] = _serialize_value(status.dirty_water_box_status)
        result["dust_bag_status"] = _serialize_value(status.dust_bag_status)
        result["water_box_filter_status"] = status.water_box_filter_status
        result["clean_fluid_status"] = _serialize_value(status.clean_fluid_status)
        result["hatch_door_status"] = status.hatch_door_status
        result["dock_cool_fan_status"] = status.dock_cool_fan_status

    # --- dnd ---
    if ok["dnd"]:
        dnd = props.dnd
        result["dnd_enabled"] = dnd.is_on
        result["dnd_start_time"] = (
            f"{dnd.start_hour:02d}:{dnd.start_minute:02d}" if dnd.is_on else None
        )
        result["dnd_end_time"] = (
            f"{dnd.end_hour:02d}:{dnd.end_minute:02d}" if dnd.is_on else None
        )

    # --- clean_summary ---
    if ok["clean_summary"]:
        summary = props.clean_summary
        result["total_clean_time_hours"] = (
            round(summary.clean_time / 3600, 1) if summary.clean_time else 0
        )
        result["total_clean_area_m2"] = (
            round(summary.clean_area / 1_000_000, 1) if summary.clean_area else 0
        )
        result["total_clean_count"] = summary.clean_count
        result["clean_record_count"] = len(summary.records) if summary.records else 0

        if summary.last_clean_record:
            record = summary.last_clean_record
            result["last_clean_begin"] = (
                record.begin_datetime.isoformat() if record.begin_datetime else None
            )
            result["last_clean_end"] = (
                record.end_datetime.isoformat() if record.end_datetime else None
            )
            result["last_clean_duration_seconds"] = record.duration
            result["last_clean_area_m2"] = (
                record.area / 1_000_000 if record.area else 0
            )
            result["last_clean_start_type"] = (
                record.start_type.name if record.start_type else None
            )
            result["last_clean_avoid_count"] = record.avoid_count
            result["last_clean_wash_count"] = record.wash_count

    # --- sound_volume ---
    if ok["sound_volume"]:
        result["volume"] = props.sound_volume.volume

    # --- rooms ---
    if ok["rooms"]:
        rooms = props.rooms
        room_list = []
        for room in rooms.rooms or []:
            room_list.append({
                "segment_id": room.segment_id,
                "iot_id": room.iot_id,
                "name": room.raw_name or "<未命名>",
            })
        result["room_count"] = len(room_list)
        result["rooms"] = room_list

    # --- maps ---
    if ok["maps"]:
        maps = props.maps
        map_list = []
        for map_info in maps.map_info or []:
            map_list.append({
                "map_flag": map_info.map_flag,
                "name": map_info.name or "<未命名>",
                "room_count": len(map_info.rooms) if map_info.rooms else 0,
                "is_current": map_info.map_flag == maps.current_map,
            })
        result["current_map_flag"] = maps.current_map
        result["map_count"] = len(map_list)
        result["maps"] = map_list

    # --- consumables ---
    if ok["consumables"]:
        consumables = props.consumables
        SECS_PER_HOUR = 3600
        result["main_brush_hours"] = (
            round(consumables.main_brush_work_time / SECS_PER_HOUR, 1)
            if consumables.main_brush_work_time else 0
        )
        result["side_brush_hours"] = (
            round(consumables.side_brush_work_time / SECS_PER_HOUR, 1)
            if consumables.side_brush_work_time else 0
        )
        result["filter_hours"] = (
            round(consumables.filter_work_time / SECS_PER_HOUR, 1)
            if consumables.filter_work_time else 0
        )
        result["sensor_hours"] = (
            round(consumables.sensor_dirty_time / SECS_PER_HOUR, 1)
            if consumables.sensor_dirty_time else 0
        )

    # --- home ---
    if ok["home"]:
        home = props.home
        home_maps = {}
        for map_flag, map_info in (home.home_map_info or {}).items():
            rooms = []
            for room in map_info.rooms or []:
                rooms.append({
                    "segment_id": room.segment_id,
                    "name": room.raw_name or "<未命名>",
                })
            home_maps[str(map_flag)] = {
                "name": map_info.name or "<未命名>",
                "rooms": rooms,
            }
        result["home_maps"] = home_maps

        home_current_rooms = []
        for room in home.current_rooms or []:
            home_current_rooms.append({
                "segment_id": room.segment_id,
                "name": room.raw_name or "<未命名>",
            })
        result["home_current_rooms"] = home_current_rooms

    # --- network_info ---
    if ok["network_info"]:
        net = props.network_info
        result["ip"] = net.ip
        result["ssid"] = net.ssid
        result["mac"] = net.mac
        result["bssid"] = net.bssid
        result["rssi"] = net.rssi

    # --- device_features ---
    if ok["device_features"]:
        features = props.device_features
        features_dict = {}
        for f in dataclasses.fields(features):
            if f.name.startswith("is_"):
                features_dict[f.name] = getattr(features, f.name, False)
        result["device_features"] = features_dict

    return result


@mcp.tool(
    annotations={
        "title": "Get Map Content",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    }
)
@_handle_errors
async def get_map_content(device_name: str | None = None) -> dict[str, Any]:
    """获取当前地图的元数据（通过 MQTT 拉取，数据量较大）。

    返回地图图片大小、机器人实时坐标、充电座坐标。不返回原始图片字节。
    此工具走 MQTT 拉取地图数据（几百KB~几MB），比 get_status 更重，
    仅在需要位置信息时调用。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。

    Returns:
        dict: {
            "device_name": str,
            "image_size_bytes": int,    # 地图图片大小
            "has_map_data": bool,       # 是否有地图数据
            "vacuum_position": {"x": int, "y": int},   # 机器人位置
            "charger_position": {"x": int, "y": int}    # 充电座位置
        }
    """
    conn = get_conn()
    device = await conn.get_device(device_name)

    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    props = device.v1_properties
    await props.map_content.refresh()
    content = props.map_content

    result = {
        "device_name": device.name,
        "image_size_bytes": len(content.image_content) if content.image_content else 0,
        "has_map_data": content.map_data is not None,
    }

    if content.map_data:
        vacuum_pos = content.map_data.vacuum_position
        charger_pos = content.map_data.charger
        result["vacuum_position"] = {
            "x": vacuum_pos.x if vacuum_pos else None,
            "y": vacuum_pos.y if vacuum_pos else None,
        }
        result["charger_position"] = {
            "x": charger_pos.x if charger_pos else None,
            "y": charger_pos.y if charger_pos else None,
        }

    return result
