"""
Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com

Async execution handler: accept request, return immediately, run command in
background and notify via job_push notifier so WS subscribers receive the result.
Returns ExecuteAsyncAcceptance shape with server-generated ws_id, job_id null,
nested delivery.
"""

from __future__ import annotations

import asyncio
from typing import Any, Dict, Optional

from fastapi import Request

from mcp_proxy_adapter.api.execute_async_contract import (
    DeliveryContract,
    ExecuteAsyncAcceptance,
)
from mcp_proxy_adapter.commands.command_registry import registry
from mcp_proxy_adapter.core.constants import (
    ASYNC_COMMAND_TIMEOUT,
    resolve_timeout,
)
from mcp_proxy_adapter.core.delivery.ws_delivery_manager import WsDeliveryManager
from mcp_proxy_adapter.core.errors import (
    InternalError,
    InvalidRequestError,
    MethodNotFoundError,
)
from mcp_proxy_adapter.core.job_push.events import build_delivery_push_message
from mcp_proxy_adapter.core.logging import RequestLogger, get_global_logger

ASYNC_ACCEPT_MESSAGE: str = "Command accepted; result will be delivered via WebSocket"

_TRANSFER_METADATA_SCALAR_KEYS: tuple[str, ...] = (
    "filename",
    "compression",
    "checksum_algorithm",
    "checksum_value",
    "size_bytes",
    "chunk_size",
)


def _params_contain_binary(params: object) -> bool:
    """Return True if params tree holds bytes/bytearray (shallow list, dict recurse)."""
    if params is None:
        return False
    if isinstance(params, dict):
        for v in params.values():
            if isinstance(v, (bytes, bytearray)):
                return True
            if isinstance(v, dict) and _params_contain_binary(v):
                return True
        return False
    if isinstance(params, list):
        for item in params:
            if isinstance(item, (bytes, bytearray)):
                return True
            if isinstance(item, dict) and _params_contain_binary(item):
                return True
        return False
    return False


async def _run_async_command_and_notify(
    command_class: Any,
    command_name: str,
    params: Dict[str, Any],
    context: Dict[str, Any],
    ws_id: str,
    manager: WsDeliveryManager,
    logger: Any,
    timeout: float = ASYNC_COMMAND_TIMEOUT,
) -> None:
    """
    Run command in background and notify terminal via delivery manager.

    Payload uses build_delivery_push_message for delivery domain events.
    """
    handoff_transfer_id: str | None = None
    if isinstance(params, dict) and params.get("transfer_id") is not None:
        handoff_transfer_id = str(params["transfer_id"])

    effective_timeout = resolve_timeout(timeout)
    try:
        try:
            result_obj = await asyncio.wait_for(
                command_class.run(**params, context=context), timeout=effective_timeout
            )
            payload = build_delivery_push_message(
                ws_id=ws_id,
                status="completed",
                result=result_obj.to_dict(),
                error=None,
                progress=100,
                description="",
            )
            if handoff_transfer_id is not None:
                payload["transfer_id"] = handoff_transfer_id
            await manager.notify_terminal_delivery(ws_id, payload)
        except asyncio.TimeoutError:
            logger.warning(
                "Async command '%s' (ws_id=%s) timed out after %.1fs",
                command_name,
                ws_id,
                timeout,
            )
            payload = build_delivery_push_message(
                ws_id=ws_id,
                status="failed",
                result=None,
                error=f"Command timed out after {timeout}s",
                progress=0,
                description="",
            )
            if handoff_transfer_id is not None:
                payload["transfer_id"] = handoff_transfer_id
            await manager.notify_terminal_delivery(ws_id, payload)
        except Exception as exc:
            logger.exception(
                "Async command '%s' (ws_id=%s) failed: %s",
                command_name,
                ws_id,
                exc,
            )
            payload = build_delivery_push_message(
                ws_id=ws_id,
                status="failed",
                result=None,
                error=str(exc),
                progress=0,
                description="",
            )
            if handoff_transfer_id is not None:
                payload["transfer_id"] = handoff_transfer_id
            await manager.notify_terminal_delivery(ws_id, payload)
    except Exception as e:
        logger.warning("Delivery notify failed for ws_id=%s: %s", ws_id, e)


async def execute_command_async(
    command_name: str,
    params: Optional[Dict[str, Any]],
    _legacy_client_delivery_id_ignored: Optional[str],
    request_id: Optional[str] = None,
    request: Optional[Request] = None,
) -> Dict[str, Any]:
    """Accept async command request, return immediately, run command in background.

    Returns ExecuteAsyncAcceptance shape (accepted, job_id, ws_id, delivery,
    message) with server-generated ws_id. On completion or failure notifies via
    notify_job_state_changed so WS subscribers receive the result.
    """
    logger = RequestLogger(__name__, request_id) if request_id else get_global_logger()

    try:
        command_class = registry.get_command(command_name)
    except Exception:
        raise MethodNotFoundError(f"Method not found: {command_name}")

    context: Dict[str, Any] = {}
    if request is not None and hasattr(request, "state"):
        user_id = getattr(request.state, "user_id", None)
        user_role = getattr(request.state, "user_role", None)
        user_roles = getattr(request.state, "user_roles", None)
        if user_id or user_role or user_roles:
            context["user"] = {
                "id": user_id,
                "role": user_role,
                "roles": user_roles or [],
            }

    if (
        _legacy_client_delivery_id_ignored is not None
        and str(_legacy_client_delivery_id_ignored).strip()
    ):
        logger.debug(
            "Ignoring client-supplied delivery id for execute_async correlation"
        )
    manager: Optional[WsDeliveryManager] = None
    if request is not None and hasattr(request, "app"):
        manager = getattr(request.app.state, "ws_delivery_manager", None)
    if manager is None:
        raise InternalError(
            "WebSocket delivery not available (ws_delivery_manager not set)"
        )

    if _params_contain_binary(params):
        raise InvalidRequestError(
            "execute_async params must not contain raw binary (bytes or bytearray)"
        )

    ws_id = manager.issue_ws_id()

    command_params = (params or {}).copy()
    command_params.pop("deliver_id", None)
    command_params.pop("ws_id", None)

    asyncio.create_task(
        _run_async_command_and_notify(
            command_class,
            command_name,
            command_params,
            context,
            ws_id,
            manager,
            logger,
        )
    )

    acceptance = ExecuteAsyncAcceptance(
        accepted=True,
        job_id=None,
        ws_id=ws_id,
        delivery=DeliveryContract(mode="ws", terminal_only=True),
        message=ASYNC_ACCEPT_MESSAGE,
    )
    acceptance_body = acceptance.model_dump(mode="json")
    if isinstance(params, dict):
        if params.get("transfer_id") is not None:
            acceptance_body["transfer_id"] = str(params["transfer_id"])
        for key in _TRANSFER_METADATA_SCALAR_KEYS:
            if key not in params or params[key] is None:
                continue
            val = params[key]
            if isinstance(val, (bytes, bytearray)):
                continue
            acceptance_body[key] = val
    return acceptance_body
