"""
Smart device control tools with async verification.

This module provides intelligent device control with domain-specific handling
and async operation verification through WebSocket monitoring.
"""

import asyncio
import json
import logging
import time
from typing import Any, ClassVar

from fastmcp import Context
from fastmcp.exceptions import ToolError

from ..client.rest_client import HomeAssistantClient
from ..client.websocket_listener import start_websocket_listener
from ..config import get_global_settings
from ..errors import ErrorCode, create_error_response
from ..utils.domain_handlers import get_domain_handler
from ..utils.operation_manager import get_operation_from_memory, store_pending_operation
from .helpers import (
    exception_to_structured_error,
    raise_tool_error,
    safe_info,
    safe_progress,
)

logger = logging.getLogger(__name__)


class DeviceControlTools:
    """Smart device control tools with async verification."""

    def __init__(self, client: HomeAssistantClient | None = None):
        """Initialize device control tools."""
        # Only load settings if client not provided
        if client is None:
            self.settings = get_global_settings()
            self.client = HomeAssistantClient()
        else:
            self.settings = None  # type: ignore[assignment]
            self.client = client
        self._listener_started = False

    async def _ensure_websocket_listener(self) -> None:
        """Ensure WebSocket listener is running for async verification."""
        if not self._listener_started:
            try:
                success = await start_websocket_listener()
                if success:
                    self._listener_started = True
                    logger.info("WebSocket listener started for async verification")
                else:
                    logger.warning(
                        "Failed to start WebSocket listener - async verification disabled"
                    )
            except Exception as e:
                logger.error(f"Error starting WebSocket listener: {e}")

    async def control_device_smart(
        self,
        entity_id: str,
        action: str,
        parameters: dict[str, Any] | None = None,
        timeout_seconds: int = 10,
        validate_first: bool = True,
    ) -> dict[str, Any]:
        """
        Universal smart device control with async verification.

        This tool provides intelligent device control with domain-specific
        parameter handling and async operation verification via WebSocket.

        Args:
            entity_id: Target entity ID (e.g., 'light.living_room')
            action: Action to perform (on, off, toggle, set, etc.)
            parameters: Action-specific parameters (brightness, temperature, etc.)
            timeout_seconds: How long to wait for operation completion
            validate_first: Whether to validate entity exists before action

        Returns:
            Operation result with follow-up instructions for async checking
        """
        await self._ensure_websocket_listener()

        try:
            parameters = self._parse_parameters(parameters, entity_id, action)

            # Parse domain from entity ID
            if "." not in entity_id:
                raise_tool_error(create_error_response(
                    ErrorCode.ENTITY_INVALID_ID,
                    f"Invalid entity ID format: {entity_id}",
                    suggestions=[
                        "Entity ID must be in format 'domain.entity_name'",
                        "Use smart_entity_search to find correct entity ID",
                    ],
                    context={"entity_id": entity_id, "action": action},
                ))

            domain = entity_id.split(".")[0]
            handler = get_domain_handler(domain)

            # Validate entity exists if requested
            current_state = None
            if validate_first:
                current_state = await self._validate_entity_exists(entity_id, action)

            # Validate action for domain
            valid_actions = handler.get("valid_actions", ["on", "off", "toggle"])
            if action not in valid_actions:
                raise_tool_error(create_error_response(
                    ErrorCode.SERVICE_INVALID_ACTION,
                    f"Invalid action '{action}' for domain '{domain}'",
                    suggestions=[
                        f"Valid actions for {domain}: {', '.join(valid_actions)}",
                        "Use 'toggle' for simple on/off control",
                    ],
                    context={"entity_id": entity_id, "action": action, "valid_actions": valid_actions},
                ))

            # Build service call
            service_call = self._build_service_call(
                entity_id, domain, action, parameters
            )

            # Predict expected state after operation
            expected_state = self._predict_expected_state(
                current_state if validate_first else None, action, parameters, domain
            )

            # Execute service call
            try:
                await self.client.call_service(
                    service_call["domain"],
                    service_call["service"],
                    service_call["data"],
                )

                # Store operation for async verification
                operation_id = store_pending_operation(
                    entity_id=entity_id,
                    action=action,
                    service_domain=service_call["domain"],
                    service_name=service_call["service"],
                    service_data=service_call["data"],
                    expected_state=expected_state,
                    timeout_ms=timeout_seconds * 1000,
                )

                return {
                    "entity_id": entity_id,
                    "action": action,
                    "parameters": parameters or {},
                    "command_sent": True,
                    "operation_id": operation_id,
                    "status": "pending_verification",
                    "message": f"Command sent to {entity_id}. Use get_device_operation_status() to verify completion.",
                    "service_call": service_call,
                    "expected_state": expected_state,
                    "timeout_seconds": timeout_seconds,
                    "follow_up": {
                        "tool": "get_device_operation_status",
                        "parameters": {
                            "operation_id": operation_id,
                            "timeout_seconds": timeout_seconds,
                        },
                    },
                }

            except ToolError:
                raise
            except Exception as e:
                exception_to_structured_error(
                    e,
                    context={"entity_id": entity_id, "action": action},
                    suggestions=[
                        "Check if entity supports this action",
                        "Verify Home Assistant connection",
                        "Check Home Assistant logs for details",
                    ],
                )

        except ToolError:
            raise
        except Exception as e:
            logger.error(f"Error in control_device_smart: {e}")
            exception_to_structured_error(
                e,
                context={"entity_id": entity_id, "action": action},
                suggestions=[
                    "Check entity ID format",
                    "Verify Home Assistant connection",
                    "Try simpler action like 'toggle'",
                ],
            )

    def _parse_parameters(
        self,
        parameters: dict[str, Any] | None,
        entity_id: str,
        action: str,
    ) -> dict[str, Any] | None:
        if parameters and isinstance(parameters, str):
            try:
                return json.loads(parameters)
            except json.JSONDecodeError:
                raise_tool_error(create_error_response(
                    ErrorCode.VALIDATION_INVALID_JSON,
                    f"Invalid JSON in parameters: {parameters}",
                    suggestions=[
                        "Parameters should be a valid JSON object",
                        "Example: {'brightness': 102, 'color_temp_kelvin': 4000}",
                    ],
                    context={"entity_id": entity_id, "action": action},
                ))
        return parameters

    async def _validate_entity_exists(
        self,
        entity_id: str,
        action: str,
    ) -> dict[str, Any]:
        """Fetch entity state, raising ToolError if the entity does not exist."""
        try:
            current_state = await self.client.get_entity_state(entity_id)
            if not current_state:
                raise_tool_error(create_error_response(
                    ErrorCode.ENTITY_NOT_FOUND,
                    f"Entity not found: {entity_id}",
                    suggestions=[
                        "Use smart_entity_search to find the correct entity",
                        "Check entity is not disabled in Home Assistant",
                    ],
                    context={"entity_id": entity_id, "action": action},
                ))
            return current_state
        except ToolError:
            raise
        except Exception as e:
            exception_to_structured_error(
                e,
                context={"entity_id": entity_id, "action": action},
                suggestions=[
                    "Check Home Assistant connection",
                    "Verify entity ID spelling",
                ],
            )
            raise  # unreachable; keeps type checker satisfied

    def _resolve_service_name(
        self,
        domain: str,
        action: str,
        parameters: dict[str, Any] | None,
    ) -> tuple[str, dict[str, Any] | None]:
        service_mapping = {
            "on": "turn_on",
            "off": "turn_off",
            "toggle": "toggle",
            "open": "open_cover" if domain == "cover" else "turn_on",
            "close": "close_cover" if domain == "cover" else "turn_off",
            "set": "turn_on" if domain == "light" else "set_temperature",
        }

        service_name = service_mapping.get(action, action)

        if domain == "climate":
            if action in ["heat", "cool", "auto"]:
                service_name = "set_hvac_mode"
                if not parameters:
                    parameters = {}
                parameters["hvac_mode"] = action
            elif action == "set":
                service_name = "set_temperature"

        elif domain == "media_player":
            if action in ["play", "pause", "stop"]:
                service_name = f"media_{action}"
            elif action == "set":
                service_name = "volume_set"

        return service_name, parameters

    _DOMAIN_PARAMS: ClassVar[dict[str, list[str]]] = {
        "light": ["brightness", "color_temp_kelvin", "rgb_color", "effect"],
        "climate": ["temperature", "target_temp_high", "target_temp_low", "hvac_mode"],
        "cover": ["position", "tilt_position"],
        "media_player": ["volume_level", "media_content_id", "media_content_type"],
    }

    @staticmethod
    def _normalize_light_color_temp(parameters: dict[str, Any]) -> None:
        """Convert deprecated color temp parameters to color_temp_kelvin."""
        if "color_temp_kelvin" in parameters:
            return
        if "kelvin" in parameters:
            parameters["color_temp_kelvin"] = parameters.pop("kelvin")
        elif "color_temp" in parameters:
            mired_val = parameters.pop("color_temp")
            if isinstance(mired_val, (int, float)) and mired_val > 0:
                parameters["color_temp_kelvin"] = round(1_000_000 / mired_val)

    def _add_domain_params(
        self,
        domain: str,
        parameters: dict[str, Any],
        service_data: dict[str, Any],
    ) -> None:
        if domain == "light":
            self._normalize_light_color_temp(parameters)

        allowed = self._DOMAIN_PARAMS.get(domain, [])
        for param in allowed:
            if param in parameters:
                service_data[param] = parameters[param]

    def _build_service_call(
        self,
        entity_id: str,
        domain: str,
        action: str,
        parameters: dict[str, Any] | None,
    ) -> dict[str, Any]:
        """Build Home Assistant service call from action and parameters."""
        service_name, parameters = self._resolve_service_name(domain, action, parameters)

        service_data: dict[str, Any] = {"entity_id": entity_id}

        if parameters:
            self._add_domain_params(domain, parameters, service_data)

        # Remove None values
        service_data = {k: v for k, v in service_data.items() if v is not None}

        return {"domain": domain, "service": service_name, "data": service_data}

    def _predict_state_from_action(
        self,
        action: str,
        current_state: dict[str, Any] | None,
    ) -> dict[str, Any] | None:
        expected: dict[str, Any] = {}
        if action == "on":
            expected["state"] = "on"
        elif action == "off":
            expected["state"] = "off"
        elif action == "toggle":
            if current_state:
                current = current_state.get("state", "off")
                expected["state"] = "off" if current == "on" else "on"
            else:
                return None
        elif action == "open":
            expected["state"] = "open"
        elif action == "close":
            expected["state"] = "closed"
        return expected

    def _predict_attributes_from_params(
        self,
        domain: str,
        action: str,
        parameters: dict[str, Any],
        expected: dict[str, Any],
    ) -> None:
        if domain == "light" and action in ["on", "set"]:
            if "brightness" in parameters:
                expected["brightness"] = parameters["brightness"]
            if "color_temp_kelvin" in parameters:
                expected["color_temp_kelvin"] = parameters["color_temp_kelvin"]

        elif domain == "climate" and action in ["set", "heat", "cool", "auto"]:
            if "temperature" in parameters:
                expected["temperature"] = parameters["temperature"]
            if "hvac_mode" in parameters:
                expected["hvac_mode"] = parameters["hvac_mode"]
            elif action in ["heat", "cool", "auto"]:
                expected["hvac_mode"] = action

    def _predict_expected_state(
        self,
        current_state: dict[str, Any] | None,
        action: str,
        parameters: dict[str, Any] | None,
        domain: str,
    ) -> dict[str, Any] | None:
        """Predict expected entity state after operation."""
        expected = self._predict_state_from_action(action, current_state)
        if expected is None:
            return None

        if parameters:
            self._predict_attributes_from_params(domain, action, parameters, expected)

        return expected if expected else None

    async def get_device_operation_status(
        self, operation_id: str, timeout_seconds: int = 10
    ) -> dict[str, Any]:
        """Check status of a device operation, waiting up to ``timeout_seconds`` for completion.

        Polls the in-memory operation registry (mutated by the WebSocket
        listener as state changes arrive) every 0.2s while the operation is
        pending, up to ``timeout_seconds``. Returns the final structured status
        — completed/failed/timeout/pending — produced by
        ``control_device_smart``.
        """
        operation = get_operation_from_memory(operation_id)

        if not operation:
            raise_tool_error(create_error_response(
                ErrorCode.RESOURCE_NOT_FOUND,
                "Operation not found or expired",
                suggestions=[
                    "Operation may have been cleaned up after completion",
                    "Check operation ID spelling",
                    "Use control_device_smart to start new operation",
                ],
                context={"operation_id": operation_id},
            ))

        # Wait up to timeout_seconds for the operation to leave the pending state.
        # The WebSocket listener mutates operation.status as state changes arrive,
        # so polling memory is sufficient — no need to subscribe again. Uses
        # time.monotonic() so the deadline can be cleanly patched in tests.
        if operation.status.value == "pending" and timeout_seconds > 0:
            deadline = time.monotonic() + timeout_seconds
            while operation.status.value == "pending":
                if time.monotonic() >= deadline:
                    break
                await asyncio.sleep(0.2)
                refreshed = get_operation_from_memory(operation_id)
                if refreshed is None:
                    raise_tool_error(create_error_response(
                        ErrorCode.RESOURCE_NOT_FOUND,
                        "Operation cleaned up during status poll",
                        suggestions=[
                            "Operation may have completed and been purged before "
                            "verification finished",
                            "Use control_device_smart to start new operation",
                        ],
                        context={"operation_id": operation_id},
                    ))
                operation = refreshed

        # Check operation status
        if operation.status.value == "completed":
            return {
                "operation_id": operation_id,
                "status": "completed",
                "success": True,
                "entity_id": operation.entity_id,
                "action": operation.action,
                "final_state": operation.result_state,
                "duration_ms": operation.duration_ms,
                "message": f"Device {operation.entity_id} successfully {operation.action}",
                "verification_method": "websocket_state_change",
                "details": {
                    "service_call": {
                        "domain": operation.service_domain,
                        "service": operation.service_name,
                        "data": operation.service_data,
                    },
                    "expected_state": operation.expected_state,
                    "actual_state": operation.result_state,
                },
            }

        elif operation.status.value == "failed":
            raise_tool_error(create_error_response(
                ErrorCode.SERVICE_CALL_FAILED,
                operation.error_message or "Device operation failed",
                context={
                    "operation_id": operation_id,
                    "entity_id": operation.entity_id,
                    "action": operation.action,
                    "duration_ms": operation.duration_ms,
                },
                suggestions=[
                    "Check if device is available and responding",
                    "Verify device supports the requested action",
                    "Check Home Assistant logs for error details",
                    "Try a simpler action like toggle",
                ],
            ))

        elif operation.status.value == "timeout":
            raise_tool_error(create_error_response(
                ErrorCode.TIMEOUT_OPERATION,
                f"Operation timed out after {operation.timeout_ms}ms",
                context={
                    "operation_id": operation_id,
                    "entity_id": operation.entity_id,
                    "action": operation.action,
                    "elapsed_ms": operation.elapsed_ms,
                },
                suggestions=[
                    "Device may be slow to respond or offline",
                    "Check device connectivity",
                    "Try increasing timeout for slow devices",
                    "Verify device is powered on",
                ],
            ))

        else:  # pending
            return {
                "operation_id": operation_id,
                "status": "pending",
                "entity_id": operation.entity_id,
                "action": operation.action,
                "elapsed_ms": operation.elapsed_ms,
                "timeout_in_ms": operation.timeout_ms,
                "time_remaining_ms": operation.timeout_ms - operation.elapsed_ms,
                "message": f"Waiting for {operation.entity_id} to respond to {operation.action}...",
                "expected_state": operation.expected_state,
                "monitoring": "websocket_state_changes",
                "tips": [
                    "Operation will auto-complete when device state changes",
                    "Physical devices may take 1-3 seconds to respond",
                    "Call this function again to check for updates",
                ],
            }

    @staticmethod
    def _validate_bulk_operations(
        operations: list[dict[str, Any]],
        skipped_operations: list[dict[str, Any]],
    ) -> list[tuple[int, dict[str, Any], str, str]]:
        valid: list[tuple[int, dict[str, Any], str, str]] = []
        for i, op in enumerate(operations):
            if not isinstance(op, dict):
                error = f"Operation at index {i} is not a dict: {type(op).__name__}"
                logger.warning(f"Bulk control: {error}")
                err_response = create_error_response(
                    ErrorCode.VALIDATION_MISSING_PARAMETER, error, context={"index": i}
                )
                err_response["index"] = i
                err_response["operation"] = op
                skipped_operations.append(err_response)
                continue

            entity_id = op.get("entity_id")
            action = op.get("action")
            missing = [f for f in ("entity_id", "action") if not op.get(f)]

            if missing:
                error = f"Operation at index {i} missing required fields: {', '.join(missing)}"
                logger.warning(f"Bulk control: {error}")
                err_response = create_error_response(
                    ErrorCode.VALIDATION_MISSING_PARAMETER, error, context={"index": i}
                )
                err_response["index"] = i
                err_response["operation"] = op
                skipped_operations.append(err_response)
            else:
                valid.append((i, op, str(entity_id), str(action)))
        return valid

    async def bulk_device_control(
        self,
        operations: list[dict[str, Any]],
        parallel: bool = True,
        ctx: Context | None = None,
    ) -> dict[str, Any]:
        """
        Control multiple devices with bulk operation support.

        Args:
            operations: List of device control operations
            parallel: Whether to execute operations in parallel

        Returns:
            Bulk operation results
        """
        if not operations:
            raise_tool_error(create_error_response(
                ErrorCode.VALIDATION_MISSING_PARAMETER,
                "No operations provided",
                suggestions=["Provide a list of device control operations"],
                context={"results": []},
            ))

        results: list[dict[str, Any]] = []
        operation_ids: list[str] = []
        skipped_operations: list[dict[str, Any]] = []

        try:
            valid_operations = self._validate_bulk_operations(
                operations, skipped_operations
            )

            await safe_info(
                ctx,
                f"bulk_device_control: {len(valid_operations)} valid op(s), "
                f"{len(skipped_operations)} skipped, "
                f"mode={'parallel' if parallel else 'sequential'}",
            )
            await safe_progress(
                ctx,
                progress=0,
                total=len(valid_operations),
                message="dispatching operations",
            )

            # Execute only valid operations
            if parallel:
                await self._execute_parallel(valid_operations, results, operation_ids)
            else:
                await self._execute_sequential(
                    valid_operations, results, operation_ids, ctx=ctx
                )

            await safe_progress(
                ctx,
                progress=len(valid_operations),
                total=len(valid_operations),
                message=(
                    f"dispatched {len(operation_ids)} op(s); "
                    "use get_bulk_operation_status to verify completion"
                ),
            )

            return self._build_bulk_response(
                operations, results, operation_ids, skipped_operations, parallel
            )

        except ToolError:
            raise
        except Exception as e:
            logger.error(f"Error in bulk_device_control: {e}")
            exception_to_structured_error(
                e,
                context={"results": results},
                suggestions=["Check operation parameters and try again"],
            )

    @staticmethod
    def _tool_error_to_dict(e: ToolError) -> dict[str, Any]:
        """Extract structured error dict from ToolError without double-encoding."""
        try:
            result: dict[str, Any] = json.loads(str(e))
        except (json.JSONDecodeError, TypeError):
            logger.warning(f"Could not decode ToolError as structured response: {e!r}")
            result = create_error_response(ErrorCode.SERVICE_CALL_FAILED, str(e))
        return result

    async def _execute_parallel(
        self,
        valid_operations: list[tuple[int, dict[str, Any], str, str]],
        results: list[dict[str, Any]],
        operation_ids: list[str],
    ) -> None:
        tasks = []
        for _i, op, entity_id, action in valid_operations:
            task = self.control_device_smart(
                entity_id=entity_id,
                action=action,
                parameters=op.get("parameters"),
                timeout_seconds=op.get("timeout_seconds", 10),
                validate_first=op.get("validate_first", True),
            )
            tasks.append(task)

        if tasks:
            task_results = await asyncio.gather(*tasks, return_exceptions=True)

            for result in task_results:
                if isinstance(result, ToolError):
                    results.append(self._tool_error_to_dict(result))
                elif isinstance(result, Exception):
                    results.append(create_error_response(
                        ErrorCode.SERVICE_CALL_FAILED,
                        f"Exception during execution: {result!s}",
                    ))
                elif isinstance(result, dict):
                    results.append(result)
                    if "operation_id" in result:
                        operation_ids.append(result["operation_id"])

    async def _execute_sequential(
        self,
        valid_operations: list[tuple[int, dict[str, Any], str, str]],
        results: list[dict[str, Any]],
        operation_ids: list[str],
        ctx: Context | None = None,
    ) -> None:
        total = len(valid_operations)
        for i, (_orig_index, op, entity_id, action) in enumerate(valid_operations):
            try:
                result = await self.control_device_smart(
                    entity_id=entity_id,
                    action=action,
                    parameters=op.get("parameters"),
                    timeout_seconds=op.get("timeout_seconds", 10),
                    validate_first=op.get("validate_first", True),
                )
                results.append(result)
                if "operation_id" in result:
                    operation_ids.append(result["operation_id"])
            except ToolError as e:
                results.append(self._tool_error_to_dict(e))
            except Exception as e:
                results.append(create_error_response(
                    ErrorCode.SERVICE_CALL_FAILED,
                    f"Exception during execution: {e!s}",
                ))
            await safe_progress(
                ctx,
                progress=i + 1,
                total=total,
                message=f"{entity_id} {action} dispatched",
            )

    def _build_bulk_response(
        self,
        operations: list[dict[str, Any]],
        results: list[dict[str, Any]],
        operation_ids: list[str],
        skipped_operations: list[dict[str, Any]],
        parallel: bool,
    ) -> dict[str, Any]:
        successful = len(
            [r for r in results if isinstance(r, dict) and r.get("command_sent")]
        )
        executed_failed = len(results) - successful
        # Total failed includes both execution failures and skipped operations
        total_failed = executed_failed + len(skipped_operations)

        response: dict[str, Any] = {
            "total_operations": len(operations),
            "successful_commands": successful,
            "failed_commands": total_failed,
            "skipped_operations": len(skipped_operations),
            "execution_mode": "parallel" if parallel else "sequential",
            "operation_ids": operation_ids,
            "results": results,
            "follow_up": (
                {
                    "message": (
                        f"Use get_bulk_operation_status() to check all "
                        f"{len(operation_ids)} operations"
                    ),
                    "operation_ids": operation_ids,
                }
                if operation_ids
                else None
            ),
        }

        # Include skipped operation details if any were skipped
        if skipped_operations:
            response["skipped_details"] = skipped_operations
            response["suggestions"] = [
                "Some operations were skipped due to validation errors",
                "Each operation requires 'entity_id' and 'action' fields",
                "Check skipped_details for specific errors",
                "Example format: {'entity_id': 'light.living_room', 'action': 'on'}",
            ]

        return response

    async def get_bulk_operation_status(
        self, operation_ids: list[str]
    ) -> dict[str, Any]:
        """
        Check status of multiple operations.

        Args:
            operation_ids: List of operation IDs to check

        Returns:
            Status summary for all operations
        """
        if not operation_ids:
            raise_tool_error(create_error_response(
                ErrorCode.VALIDATION_MISSING_PARAMETER,
                "No operation IDs provided",
                suggestions=["Provide a list of operation IDs from control_device_smart"],
            ))

        # Check all operations
        statuses = []
        for op_id in operation_ids:
            status = await self.get_device_operation_status(op_id)
            statuses.append(status)

        # Summarize results
        completed = len([s for s in statuses if s.get("status") == "completed"])
        failed = len([s for s in statuses if s.get("status") in ["failed", "timeout"]])
        pending = len([s for s in statuses if s.get("status") == "pending"])

        return {
            "total_operations": len(operation_ids),
            "completed": completed,
            "failed": failed,
            "pending": pending,
            "all_complete": pending == 0,
            "summary": {
                "success_rate": f"{completed}/{len(operation_ids)}",
                "completion_percentage": (completed / len(operation_ids)) * 100,
            },
            "detailed_results": statuses,
            "recommendations": (
                [
                    "Wait a few seconds and check again if operations are pending",
                    "Check failed operations for specific error messages",
                    "Retry failed operations with different parameters if needed",
                ]
                if pending > 0 or failed > 0
                else ["All operations completed successfully!"]
            ),
        }


def create_device_control_tools(
    client: HomeAssistantClient | None = None,
) -> DeviceControlTools:
    """Create device control tools instance."""
    return DeviceControlTools(client)
