"""Tool for running manual tasks on a connected mobile device."""

from collections.abc import Mapping
from pathlib import Path
from typing import Any

from fastmcp.exceptions import ToolError
from fastmcp.tools.tool import ToolResult
from mcp.types import TextContent
from minitap.mobile_use.sdk import Agent
from minitap.mobile_use.sdk.builders import Builders
from minitap.mobile_use.sdk.types import LimrunPlatform, ManualTaskConfig
from minitap.mobile_use.sdk.types.task import PlatformTaskRequest
from pydantic import Field

from minitap.mcp.core.config import settings
from minitap.mcp.core.decorators import handle_tool_errors
from minitap.mcp.core.logging_config import get_logger
from minitap.mcp.core.sdk_agent import get_mobile_use_agent
from minitap.mcp.core.storage import StorageDownloadError, download_trajectory_gif
from minitap.mcp.core.task_runs import TaskRunsError, get_latest_task_run_id
from minitap.mcp.main import mcp

logger = get_logger(__name__)


def _serialize_result(result: Any) -> Any:
    """Convert SDK responses to serializable data for MCP."""
    if hasattr(result, "model_dump"):
        return result.model_dump()
    if hasattr(result, "dict"):
        return result.dict()
    if isinstance(result, Mapping):
        return dict(result)
    return result


@mcp.tool(
    name="execute_mobile_command",
    description="""
    Execute a natural language command on a mobile device using the Minitap SDK.
    This tool allows you to control Android or iOS devices using natural language.
    
    Set cloud_platform to run on a cloud device (recommended for most use cases).
    
    Examples:
    - execute_mobile_command(cloud_platform="android", goal="Open Settings and check battery level")
    - execute_mobile_command(cloud_platform="ios", goal="Open Safari and search for weather")
    
    App Deployment:
    You can deploy and test apps by providing the path to a locally built app:
    - Set app_path to the path of your .apk file (Android) or .app folder (iOS)
    - The app will be automatically uploaded and installed on the device
    - Must provide locked_app_package (package name or bundle ID) when using app_path
    
    Examples with app deployment:
    - execute_mobile_command(cloud_platform="android", goal="Test login flow", 
        app_path="/path/to/app-debug.apk", locked_app_package="com.example.myapp")
    - execute_mobile_command(cloud_platform="ios", goal="Verify onboarding screens",
        app_path="/path/to/MyApp.app", locked_app_package="com.example.myapp")
    """,
)
@handle_tool_errors
async def execute_mobile_command(
    goal: str = Field(description="High-level goal describing the action to perform."),
    cloud_platform: LimrunPlatform | None = Field(
        default=None,
        description="Cloud platform to run on: 'android' or 'ios'. "
        "When set, a cloud device is automatically provisioned for this task "
        "and cleaned up afterwards.",
    ),
    output_description: str | None = Field(
        default=None,
        description="Optional description of the expected output format. "
        "For example: 'A JSON array with sender and subject for each email' "
        "or 'The battery percentage as a number'.",
    ),
    locked_app_package: str | None = Field(
        default=None,
        description="Optional package name of the app to lock the device to. "
        "Will launch the app if not already running, and keep it in foreground "
        "until the task is completed. REQUIRED when using app_path.",
    ),
    app_path: str | None = Field(
        default=None,
        description="Path to local .apk file (Android) or .app folder (iOS) to deploy. "
        "The app will be automatically uploaded and installed on the device. "
        "Must provide locked_app_package when using app_path.",
    ),
) -> str | dict[str, Any] | ToolResult:
    """Run a manual task on a mobile device via the Minitap platform."""
    try:
        # Cloud platform mode: provision a cloud device on-demand
        if cloud_platform:
            return await _execute_with_cloud_platform(
                cloud_platform=cloud_platform,
                goal=goal,
                output_description=output_description,
                locked_app_package=locked_app_package,
                app_path=app_path,
            )

        # Local device mode
        if app_path:
            raise ToolError(
                "app_path parameter requires cloud_platform to be set. "
                "App deployment is only supported in cloud platform mode."
            )

        request = PlatformTaskRequest(
            task=ManualTaskConfig(
                goal=goal,
                output_description=output_description,
            ),
            execution_origin="mcp",
        )
        agent = get_mobile_use_agent()
        if not agent._initialized:
            await agent.init()
        result = await agent.run_task(
            request=request,
            locked_app_package=locked_app_package,
        )

        trajectory_gif_path: Path | None = None
        if settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER:
            trajectory_gif_path = await _download_trajectory_gif_if_available()

        serialized_result = _serialize_result(result)

        # If trajectory was saved, return a ToolResult with multiple content items
        if trajectory_gif_path:
            import json

            result_text = (
                json.dumps(serialized_result, indent=2)
                if isinstance(serialized_result, dict)
                else str(serialized_result)
            )
            return ToolResult(
                content=[
                    TextContent(type="text", text=result_text),
                    TextContent(type="text", text=f"Trajectory saved to {trajectory_gif_path}"),
                ],
            )

        return serialized_result
    except Exception as e:
        raise ToolError(str(e))


async def _execute_with_cloud_platform(
    cloud_platform: LimrunPlatform,
    goal: str,
    output_description: str | None,
    locked_app_package: str | None,
    app_path: str | None = None,
) -> str | dict[str, Any] | ToolResult:
    """Execute a task using a cloud device.

    Provisions a cloud device, runs the task, and cleans up afterwards.
    """
    api_key = settings.MINITAP_API_KEY.get_secret_value() if settings.MINITAP_API_KEY else None
    if not api_key:
        raise ToolError(
            "MINITAP_API_KEY is required for cloud platform mode. "
            "Please set your API key via --api-key or MINITAP_API_KEY environment variable."
        )

    logger.info(f"Provisioning {cloud_platform.value} cloud device for task execution...")

    # Create a dedicated agent for this cloud task
    config = Builders.AgentConfig.for_limrun(platform=cloud_platform, api_key=api_key)
    agent = Agent(config=config.build())

    try:
        await agent.init(api_key=api_key)
        logger.info(f"{cloud_platform.value} cloud device ready, executing task...")

        request = PlatformTaskRequest(
            task=ManualTaskConfig(
                goal=goal,
                output_description=output_description,
            ),
            execution_origin="mcp",
        )

        # Pass app_path to run_task - SDK handles upload and installation
        result = await agent.run_task(
            request=request,
            locked_app_package=locked_app_package,
            app_path=app_path,
        )

        trajectory_gif_path: Path | None = None
        if settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER:
            trajectory_gif_path = await _download_trajectory_gif_if_available()

        serialized_result = _serialize_result(result)

        if trajectory_gif_path:
            import json

            result_text = (
                json.dumps(serialized_result, indent=2)
                if isinstance(serialized_result, dict)
                else str(serialized_result)
            )
            return ToolResult(
                content=[
                    TextContent(type="text", text=result_text),
                    TextContent(type="text", text=f"Trajectory saved to {trajectory_gif_path}"),
                ],
            )

        return serialized_result
    finally:
        # Always clean up the cloud device
        logger.info(f"Cleaning up {cloud_platform.value} cloud device...")
        try:
            await agent.clean()
            logger.info(f"{cloud_platform.value} cloud device cleaned up successfully")
        except Exception as e:
            logger.error(f"Error cleaning up cloud device: {e}")


async def _download_trajectory_gif_if_available() -> Path | None:
    """Download the trajectory GIF if available and folder is configured.

    Fetches the latest task run ID from the API and downloads the GIF.

    Returns:
        The path to the downloaded GIF file, or None if download failed or not configured.
    """
    download_folder = settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER
    if not download_folder:
        logger.warning("TRAJECTORY_GIF_DOWNLOAD_FOLDER not configured, skipping GIF download")
        return None

    task_run_id = None
    try:
        task_run_id = await get_latest_task_run_id()

        gif_path = await download_trajectory_gif(
            task_run_id=task_run_id,
            download_path=download_folder,
        )
        logger.info(
            "Trajectory GIF downloaded",
            task_run_id=task_run_id,
            path=str(gif_path),
        )
        return gif_path
    except (StorageDownloadError, TaskRunsError) as e:
        logger.warning(
            "Failed to download trajectory GIF",
            task_run_id=task_run_id,
            error=str(e),
        )
        return None
