"""
Structured error handling for Home Assistant MCP Server.

This module provides standardized error codes, error response models, and helper
functions for creating consistent, informative error responses across all MCP tools.

The structured error format enables AI agents to:
- Diagnose issues programmatically using error codes
- Provide helpful suggestions to users
- Understand the context and details of failures
"""

from enum import StrEnum
from typing import Any


class ErrorCode(StrEnum):
    """
    Standard error codes for Home Assistant MCP operations.

    Error codes are grouped by category:
    - CONNECTION_*: Network and connectivity issues
    - AUTH_*: Authentication and authorization issues
    - ENTITY_*: Entity-related issues
    - SERVICE_*: Service call issues
    - CONFIG_*: Configuration issues
    - VALIDATION_*: Input validation issues
    - TIMEOUT_*: Timeout issues
    - INTERNAL_*: Internal server errors
    """

    # Connection errors
    CONNECTION_FAILED = "CONNECTION_FAILED"
    CONNECTION_TIMEOUT = "CONNECTION_TIMEOUT"
    WEBSOCKET_DISCONNECTED = "WEBSOCKET_DISCONNECTED"
    WEBSOCKET_NOT_AUTHENTICATED = "WEBSOCKET_NOT_AUTHENTICATED"

    # Authentication errors
    AUTH_INVALID_TOKEN = "AUTH_INVALID_TOKEN"
    AUTH_EXPIRED = "AUTH_EXPIRED"
    AUTH_INSUFFICIENT_PERMISSIONS = "AUTH_INSUFFICIENT_PERMISSIONS"

    # Entity errors
    ENTITY_NOT_FOUND = "ENTITY_NOT_FOUND"
    ENTITY_UNAVAILABLE = "ENTITY_UNAVAILABLE"
    ENTITY_INVALID_ID = "ENTITY_INVALID_ID"
    ENTITY_DOMAIN_MISMATCH = "ENTITY_DOMAIN_MISMATCH"

    # Service errors
    SERVICE_NOT_FOUND = "SERVICE_NOT_FOUND"
    SERVICE_INVALID_DOMAIN = "SERVICE_INVALID_DOMAIN"
    SERVICE_INVALID_ACTION = "SERVICE_INVALID_ACTION"
    SERVICE_CALL_FAILED = "SERVICE_CALL_FAILED"

    # Configuration errors
    CONFIG_NOT_FOUND = "CONFIG_NOT_FOUND"
    CONFIG_INVALID = "CONFIG_INVALID"
    CONFIG_MISSING_REQUIRED_FIELDS = "CONFIG_MISSING_REQUIRED_FIELDS"
    CONFIG_VALIDATION_FAILED = "CONFIG_VALIDATION_FAILED"

    # Validation errors
    VALIDATION_FAILED = "VALIDATION_FAILED"
    VALIDATION_INVALID_JSON = "VALIDATION_INVALID_JSON"
    VALIDATION_INVALID_PARAMETER = "VALIDATION_INVALID_PARAMETER"
    VALIDATION_MISSING_PARAMETER = "VALIDATION_MISSING_PARAMETER"

    # Timeout errors
    TIMEOUT_OPERATION = "TIMEOUT_OPERATION"
    TIMEOUT_WEBSOCKET = "TIMEOUT_WEBSOCKET"
    TIMEOUT_API_REQUEST = "TIMEOUT_API_REQUEST"

    # Internal errors
    INTERNAL_ERROR = "INTERNAL_ERROR"
    INTERNAL_UNEXPECTED = "INTERNAL_UNEXPECTED"

    # Resource errors
    RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
    RESOURCE_ALREADY_EXISTS = "RESOURCE_ALREADY_EXISTS"
    RESOURCE_LOCKED = "RESOURCE_LOCKED"

    # Component errors
    COMPONENT_NOT_INSTALLED = "COMPONENT_NOT_INSTALLED"

    # Code-mode sandbox errors. The sandbox is a separate execution
    # context; runtime failures inside it map cleanly to one of these
    # three buckets so the LLM can self-recover instead of seeing every
    # failure as INTERNAL_ERROR.
    SANDBOX_LIMIT_EXCEEDED = "SANDBOX_LIMIT_EXCEEDED"
    SANDBOX_SYNTAX_UNSUPPORTED = "SANDBOX_SYNTAX_UNSUPPORTED"
    SANDBOX_RUNTIME_ERROR = "SANDBOX_RUNTIME_ERROR"

    # Tool security policy gating (#966). The middleware gates a tool
    # call awaiting user approval, the user denied it, or the policy
    # file itself failed to load (treated as a fail-closed safety stop).
    USER_APPROVAL_REQUIRED = "USER_APPROVAL_REQUIRED"
    USER_DENIED = "USER_DENIED"
    POLICY_LOAD_FAILED = "POLICY_LOAD_FAILED"

    # Read Only Mode (discussion #1569). A write operation was blocked
    # because the server-wide Read Only Mode toggle is on.
    READ_ONLY_MODE = "READ_ONLY_MODE"

    # Mandatory auto-backup (#1579). A write that requires a pre-write
    # snapshot was blocked because the snapshot could not be captured
    # (fetch failed, the snapshot write failed — e.g. disk full — or the
    # backup directory is unusable). Fail-closed: nothing was changed.
    BACKUP_CAPTURE_FAILED = "BACKUP_CAPTURE_FAILED"


# Default suggestions for common error codes
DEFAULT_SUGGESTIONS: dict[ErrorCode, list[str]] = {
    ErrorCode.CONNECTION_FAILED: [
        "Check if Home Assistant is running and accessible",
        "Verify the HOMEASSISTANT_URL environment variable is correct",
        "Check network connectivity to Home Assistant",
    ],
    ErrorCode.CONNECTION_TIMEOUT: [
        "Home Assistant may be overloaded or slow to respond",
        "Check network latency to Home Assistant",
        "Try increasing the timeout value if available",
    ],
    ErrorCode.WEBSOCKET_DISCONNECTED: [
        "WebSocket connection was lost",
        "Check Home Assistant logs for errors",
        "The operation may need to be retried",
    ],
    ErrorCode.WEBSOCKET_NOT_AUTHENTICATED: [
        "WebSocket connection is not authenticated",
        "Verify the access token is valid",
        "Try reconnecting to Home Assistant",
    ],
    ErrorCode.AUTH_INVALID_TOKEN: [
        "Verify the HOMEASSISTANT_TOKEN is correct",
        "Generate a new long-lived access token in Home Assistant",
        "Check token has not been revoked",
    ],
    ErrorCode.AUTH_EXPIRED: [
        "The access token has expired",
        "Generate a new long-lived access token in Home Assistant",
    ],
    ErrorCode.AUTH_INSUFFICIENT_PERMISSIONS: [
        "The access token does not have sufficient permissions",
        "Check token permissions in Home Assistant",
        "Create a new token with required permissions",
    ],
    ErrorCode.ENTITY_NOT_FOUND: [
        "Use ha_search() to find correct entity ID",
        "Verify the entity exists in Home Assistant",
        "Check for typos in the entity ID",
    ],
    ErrorCode.ENTITY_UNAVAILABLE: [
        "The entity exists but is currently unavailable",
        "Check if the device is powered on and connected",
        "Check Home Assistant integration status",
    ],
    ErrorCode.ENTITY_INVALID_ID: [
        "Entity ID must be in format: domain.name",
        "Use ha_search() to find valid entity IDs",
    ],
    ErrorCode.ENTITY_DOMAIN_MISMATCH: [
        "Cannot change entity to a different domain",
        "Entity domain must match the original domain",
    ],
    ErrorCode.SERVICE_NOT_FOUND: [
        "Use ha_get_skill_guide for documentation",
        "Check the service name spelling",
        "Verify the domain supports this service",
    ],
    ErrorCode.SERVICE_INVALID_DOMAIN: [
        "Use ha_get_overview() to see available domains",
        "Check the domain name spelling",
    ],
    ErrorCode.SERVICE_INVALID_ACTION: [
        "Check available actions for this domain",
        "Common actions: turn_on, turn_off, toggle",
        "Use ha_get_skill_guide for documentation",
    ],
    ErrorCode.SERVICE_CALL_FAILED: [
        "Check the service parameters are correct",
        "Verify the target entity supports this service",
        "Check Home Assistant logs for detailed error",
    ],
    ErrorCode.CONFIG_NOT_FOUND: [
        "Verify the resource exists",
        "Use search tools to find the correct identifier",
    ],
    ErrorCode.CONFIG_INVALID: [
        "Review the configuration format",
        "Use ha_get_skill_guide for configuration help",
    ],
    ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS: [
        "Check documentation for required fields",
        "Ensure all required parameters are provided",
    ],
    ErrorCode.VALIDATION_INVALID_JSON: [
        "Ensure the parameter is valid JSON",
        "Check for syntax errors in JSON",
        "Use a JSON validator to verify the format",
    ],
    ErrorCode.VALIDATION_INVALID_PARAMETER: [
        "Check the parameter type and format",
        "Review the tool documentation for expected values",
    ],
    ErrorCode.TIMEOUT_OPERATION: [
        "The operation took too long to complete",
        "Home Assistant may be under heavy load",
        "Try the operation again",
    ],
    ErrorCode.INTERNAL_ERROR: [
        "An internal error occurred",
        "Check Home Assistant MCP server logs",
        "Report this issue if it persists",
    ],
    ErrorCode.COMPONENT_NOT_INSTALLED: [
        "Install the required custom component via HACS",
        "Restart Home Assistant after installation",
    ],
}


def create_error_response(
    code: ErrorCode,
    message: str,
    details: str | None = None,
    suggestions: list[str] | None = None,
    context: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """
    Create a structured error response.

    Args:
        code: Error code from ErrorCode enum
        message: Human-readable error message
        details: Additional details about the error (optional)
        suggestions: List of suggestions to resolve the error (optional)
        context: Additional context data (e.g., entity_id, domain) (optional)

    Returns:
        Structured error response dictionary with success=False

    Example:
        >>> create_error_response(
        ...     ErrorCode.ENTITY_NOT_FOUND,
        ...     "Entity light.nonexistent not found",
        ...     details="No entity with this ID exists in Home Assistant",
        ...     context={"entity_id": "light.nonexistent"}
        ... )
        {
            "success": False,
            "error": {
                "code": "ENTITY_NOT_FOUND",
                "message": "Entity light.nonexistent not found",
                "details": "No entity with this ID exists in Home Assistant",
                "suggestion": "Use ha_search() to find correct entity ID"
            },
            "entity_id": "light.nonexistent"
        }
    """
    # Use provided suggestions or fall back to defaults
    error_suggestions = (
        suggestions if suggestions else DEFAULT_SUGGESTIONS.get(code, [])
    )

    error_dict: dict[str, Any] = {
        "code": code.value,
        "message": message,
    }

    if details:
        error_dict["details"] = details

    if error_suggestions:
        # Include first suggestion as primary, all suggestions in list
        error_dict["suggestion"] = error_suggestions[0]
        if len(error_suggestions) > 1:
            error_dict["suggestions"] = error_suggestions

    response: dict[str, Any] = {
        "success": False,
        "error": error_dict,
    }

    # Add context fields at top level for easy access
    if context:
        response.update(context)

    return response


def create_connection_error(
    message: str,
    details: str | None = None,
    timeout: bool = False,
    context: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Create a connection error response."""
    code = ErrorCode.CONNECTION_TIMEOUT if timeout else ErrorCode.CONNECTION_FAILED
    return create_error_response(code, message, details, context=context)


# Authentication-error suggestions for Home Assistant add-on installs. On the
# add-on the token is managed by the Supervisor, not a user-editable
# HOMEASSISTANT_TOKEN, so the default "verify your token / create a long-lived
# token" guidance is a dead end. We deliberately do NOT assert a cause — issue
# #1694's underlying cause was never reproduced or confirmed — so this points at
# practical first steps and at where an auth problem actually surfaces
# (home-assistant.log), without claiming to know why it happened.
_ADDON_AUTH_INVALID_SUGGESTIONS: list[str] = [
    "On a Home Assistant add-on install the access token is managed by the "
    + "Supervisor, not a HOMEASSISTANT_TOKEN you can change",
    "A Home Assistant Core restart (Settings -> System -> Restart) often "
    + "clears transient WebSocket authentication failures — try that first",
    "Check home-assistant.log for 'invalid authentication' or "
    + "'InsecureKeyLengthWarning' around the time of the failure; if those "
    + "appear, the problem is likely in Home Assistant's auth handling rather "
    + "than ha-mcp's request — capture those lines when reporting",
]


def create_auth_error(
    message: str,
    details: str | None = None,
    expired: bool = False,
    context: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Create an authentication error response.

    On a Home Assistant add-on install (Supervisor-managed) the token is the
    Supervisor token rather than a user-editable HOMEASSISTANT_TOKEN, so the
    default token-centric suggestions don't apply. Substitute add-on-specific
    guidance for an invalid token in that case (see issue #1694). Expired-token
    errors keep the default guidance — token expiry is not the Supervisor-token
    failure mode this swap targets.
    """
    code = ErrorCode.AUTH_EXPIRED if expired else ErrorCode.AUTH_INVALID_TOKEN

    suggestions: list[str] | None = None
    if code is ErrorCode.AUTH_INVALID_TOKEN:
        # Lazy import keeps this low-level module free of an import-time
        # dependency on ._version; is_running_in_addon reads SUPERVISOR_TOKEN
        # live so it reflects the current environment.
        from ._version import is_running_in_addon

        if is_running_in_addon():
            suggestions = _ADDON_AUTH_INVALID_SUGGESTIONS

    return create_error_response(
        code, message, details, suggestions=suggestions, context=context
    )


def create_entity_not_found_error(
    entity_id: str,
    details: str | None = None,
) -> dict[str, Any]:
    """Create an entity not found error response."""
    return create_error_response(
        ErrorCode.ENTITY_NOT_FOUND,
        f"Entity '{entity_id}' not found",
        details=details or f"No entity with ID '{entity_id}' exists in Home Assistant",
        context={"entity_id": entity_id},
    )


def create_service_error(
    domain: str,
    service: str,
    message: str,
    details: str | None = None,
    entity_id: str | None = None,
) -> dict[str, Any]:
    """Create a service call error response."""
    context: dict[str, Any] = {"domain": domain, "service": service}
    if entity_id:
        context["entity_id"] = entity_id

    return create_error_response(
        ErrorCode.SERVICE_CALL_FAILED,
        message,
        details=details,
        context=context,
    )


def create_validation_error(
    message: str,
    parameter: str | None = None,
    details: str | None = None,
    invalid_json: bool = False,
    context: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Create a validation error response."""
    code = (
        ErrorCode.VALIDATION_INVALID_JSON
        if invalid_json
        else ErrorCode.VALIDATION_FAILED
    )
    # Build context, prioritizing explicit context but adding parameter if provided
    final_context: dict[str, Any] = {}
    if context:
        final_context.update(context)
    if parameter:
        final_context["parameter"] = parameter
    return create_error_response(
        code, message, details, context=final_context if final_context else None
    )


def create_config_error(
    message: str,
    identifier: str | None = None,
    missing_fields: list[str] | None = None,
    details: str | None = None,
) -> dict[str, Any]:
    """Create a configuration error response."""
    if missing_fields:
        code = ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS
        details = details or f"Missing required fields: {', '.join(missing_fields)}"
    else:
        code = ErrorCode.CONFIG_INVALID

    context: dict[str, Any] = {}
    if identifier:
        context["identifier"] = identifier
    if missing_fields:
        context["missing_fields"] = missing_fields

    return create_error_response(code, message, details, context=context or None)


def create_timeout_error(
    operation: str,
    timeout_seconds: float,
    details: str | None = None,
    context: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Create a timeout error response."""
    final_context: dict[str, Any] = {}
    if context:
        final_context.update(context)
    final_context["operation"] = operation
    final_context["timeout_seconds"] = timeout_seconds
    return create_error_response(
        ErrorCode.TIMEOUT_OPERATION,
        f"Operation '{operation}' timed out after {timeout_seconds}s",
        details=details,
        context=final_context,
    )


def is_error_response(response: dict[str, Any]) -> bool:
    """Check if a response is an error response."""
    return response.get("success") is False and "error" in response


def get_error_code(response: dict[str, Any]) -> str | None:
    """Extract the error code from an error response."""
    if is_error_response(response):
        error = response.get("error", {})
        if isinstance(error, dict):
            return error.get("code")
    return None


def get_error_message(response: dict[str, Any]) -> str | None:
    """Extract the error message from an error response."""
    if is_error_response(response):
        error = response.get("error", {})
        if isinstance(error, dict):
            return error.get("message")
        if isinstance(error, str):
            return error
    return None
