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

import json
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 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.main import mcp
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 CloudDevicePlatformTaskRequest, PlatformTaskRequest

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,
        )

        await request.task_run_id_available_event.wait()
        task_run_id: str | None = request.task_run_id

        trajectory_gif_path: Path | None = None
        if settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER and task_run_id:
            trajectory_gif_path = await _download_trajectory_gif(task_run_id)

        serialized_result = _serialize_result(result)

        result_text = (
            json.dumps(serialized_result, indent=2)
            if isinstance(serialized_result, dict)
            else str(serialized_result)
        )
        content_blocks = [TextContent(type="text", text=result_text)]
        content_blocks.append(
            TextContent(type="text", text=f"gif_path: {trajectory_gif_path or 'not available'}")
        )
        content_blocks.append(
            TextContent(type="text", text=f"task_run_id: {task_run_id or 'not available'}")
        )
        return ToolResult(content=content_blocks)
    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 = CloudDevicePlatformTaskRequest(
            task=ManualTaskConfig(
                goal=goal,
                output_description=output_description,
            ),
            execution_origin="mcp",
        )

        result = await agent.run_task(
            request=request,
            locked_app_package=locked_app_package,
            app_path=app_path,
        )

        await request.task_run_id_available_event.wait()
        task_run_id: str | None = request.task_run_id

        trajectory_gif_path: Path | None = None
        if settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER and task_run_id:
            trajectory_gif_path = await _download_trajectory_gif(task_run_id)

        serialized_result = _serialize_result(result)

        result_text = (
            json.dumps(serialized_result, indent=2)
            if isinstance(serialized_result, dict)
            else str(serialized_result)
        )
        # Always return structured content with task_run_id for deterministic extraction
        content_blocks = [TextContent(type="text", text=result_text)]
        content_blocks.append(
            TextContent(type="text", text=f"gif_path: {trajectory_gif_path or 'not available'}")
        )
        # Always include task_run_id explicitly for deterministic extraction by coder
        content_blocks.append(
            TextContent(type="text", text=f"task_run_id: {task_run_id or 'not available'}")
        )
        return ToolResult(content=content_blocks)
    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(task_run_id: str) -> Path | None:
    """Download the trajectory GIF for a task run.

    Args:
        task_run_id: The task run ID to download the GIF for.

    Returns:
        Path to the downloaded GIF, or None if download failed.
    """
    download_folder = settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER
    assert download_folder is not None  # Caller must check this before calling

    try:
        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 as e:
        logger.warning(
            "Failed to download trajectory GIF",
            task_run_id=task_run_id,
            error=str(e),
        )
        return None
