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

HTTP handlers for the MCP Proxy Adapter API.
Provides JSON-RPC handling and health/commands endpoints.
Large-file transfer upload/download HTTP handlers are included in this module.
"""

from __future__ import annotations

import asyncio
import time
from typing import Any, Dict, List, Mapping, Optional, cast

from fastapi import Query
from fastapi import Request
from fastapi.responses import JSONResponse, Response

from mcp_proxy_adapter.api.handlers_async import execute_command_async
from mcp_proxy_adapter.api.transfer_session_service import (
    TransferPayloadValidationError,
    TransferSessionKindMismatch,
    run_complete_upload_session,
    run_create_download_session,
    run_create_upload_session,
    run_get_download_session,
    run_get_upload_session,
)
from mcp_proxy_adapter.commands.command_registry import registry
from mcp_proxy_adapter.core.constants import SYNC_COMMAND_TIMEOUT, resolve_timeout
from mcp_proxy_adapter.core.errors import (
    MicroserviceError,
    InvalidRequestError,
    MethodNotFoundError,
    InternalError,
    ValidationError,
    TimeoutError,
)
from mcp_proxy_adapter.core.job_push.events import (
    EVENT_TRANSFER_ACCEPTED,
    EVENT_TRANSFER_COMPLETED,
    EVENT_TRANSFER_EXPIRED,
    EVENT_TRANSFER_FAILED,
    EVENT_TRANSFER_PROGRESS,
)
from mcp_proxy_adapter.core.job_push.transfer_push import (
    build_transfer_expired_ws_payload,
    coerce_ws_push_enabled,
    notify_transfer_state_changed,
)
from mcp_proxy_adapter.transfer.progress_percent import (
    compute_transfer_progress_percent,
)
from mcp_proxy_adapter.core.logging import get_global_logger, RequestLogger
from mcp_proxy_adapter.transfer import TransferAckError
from mcp_proxy_adapter.transfer import TransferChecksumMismatchError
from mcp_proxy_adapter.transfer import TransferCompressionError
from mcp_proxy_adapter.transfer import TransferDownloadReceipt
from mcp_proxy_adapter.transfer import TransferError
from mcp_proxy_adapter.transfer import TransferExpiredError
from mcp_proxy_adapter.transfer import TransferRangeNotSatisfiableError
from mcp_proxy_adapter.transfer import TransferReceipt
from mcp_proxy_adapter.transfer import TransferResumeConflictError
from mcp_proxy_adapter.transfer import TransferServerStore
from mcp_proxy_adapter.transfer import TransferSessionNotFoundError
from mcp_proxy_adapter.transfer import TransferTooLargeError

DEFAULT_STORAGE_DIR = "/tmp/mcp_proxy_adapter/transfers"
DEFAULT_SESSION_TTL_SECONDS = 86400
DEFAULT_MAX_FILE_SIZE_BYTES = 10737418240
DEFAULT_DEFAULT_CHUNK_SIZE_BYTES = 1048576
DEFAULT_MAX_CHUNK_SIZE_BYTES = 8388608
DEFAULT_CHECKSUM_ALGORITHM = "sha256"

_CACHED_TRANSFER_STORE: TransferServerStore | None = None

_transfer_ws_progress_last_mono: dict[str, float] = {}
_TRANSFER_WS_PROGRESS_MIN_INTERVAL_SEC: float = 1.0


def _int_from_merged_config(value: object) -> int:
    """Coerce merged transfer config values to int (same inputs as built-in ``int``)."""
    return int(cast(Any, value))


def build_transfer_error_response(
    error: TransferError,
) -> tuple[int, dict[str, object]]:
    details: dict[str, object] = error.to_dict()
    error_type: str = str(details.get("error_type", type(error).__name__))
    message: str = str(details.get("message", str(error)))
    body: dict[str, object] = {
        "success": False,
        "error_type": error_type,
        "message": message,
        "details": details,
    }
    status: int
    if isinstance(error, TransferSessionNotFoundError):
        status = 404
    elif isinstance(error, TransferExpiredError):
        status = 410
    elif isinstance(error, TransferResumeConflictError):
        status = 409
    elif isinstance(error, TransferTooLargeError):
        status = 413
    elif isinstance(error, TransferRangeNotSatisfiableError):
        status = 416
    elif isinstance(error, TransferChecksumMismatchError):
        status = 422
    elif isinstance(error, TransferAckError):
        status = 422
    elif isinstance(error, TransferCompressionError):
        status = 422
    else:
        status = 400
    return (status, body)


def _merge_transfer_config() -> dict[str, object]:
    result: dict[str, object] = {
        "storage_dir": DEFAULT_STORAGE_DIR,
        "session_ttl_seconds": DEFAULT_SESSION_TTL_SECONDS,
        "max_file_size_bytes": DEFAULT_MAX_FILE_SIZE_BYTES,
        "default_chunk_size_bytes": DEFAULT_DEFAULT_CHUNK_SIZE_BYTES,
        "max_chunk_size_bytes": DEFAULT_MAX_CHUNK_SIZE_BYTES,
        "checksum_algorithm": DEFAULT_CHECKSUM_ALGORITHM,
        "server_gzip_download_buffer_dir": None,
        "ws_push_enabled": True,
    }
    try:
        from mcp_proxy_adapter.settings import get_settings  # type: ignore[import-untyped]
    except ImportError:
        pass
    else:
        settings_obj = get_settings()
        transfer_obj = getattr(settings_obj, "transfer", None)
        if transfer_obj is not None:
            raw: Mapping[str, object]
            if hasattr(transfer_obj, "model_dump"):
                raw = transfer_obj.model_dump()
            elif isinstance(transfer_obj, dict):
                raw = transfer_obj
            else:
                raw = {
                    k: getattr(transfer_obj, k)
                    for k in result.keys()
                    if hasattr(transfer_obj, k)
                }
            for k in result:
                if k not in raw or raw[k] is None:
                    continue
                if k == "server_gzip_download_buffer_dir":
                    result[k] = str(raw[k])
                elif k == "ws_push_enabled":
                    result[k] = coerce_ws_push_enabled(raw[k])
                else:
                    result[k] = raw[k]
    result["session_ttl_seconds"] = _int_from_merged_config(
        result["session_ttl_seconds"]
    )
    result["max_file_size_bytes"] = _int_from_merged_config(
        result["max_file_size_bytes"]
    )
    result["default_chunk_size_bytes"] = _int_from_merged_config(
        result["default_chunk_size_bytes"]
    )
    result["max_chunk_size_bytes"] = _int_from_merged_config(
        result["max_chunk_size_bytes"]
    )
    result["storage_dir"] = str(result["storage_dir"])
    result["checksum_algorithm"] = str(result["checksum_algorithm"])
    gzip_buf = result.get("server_gzip_download_buffer_dir")
    if gzip_buf is not None:
        result["server_gzip_download_buffer_dir"] = str(gzip_buf)
    ttl = _int_from_merged_config(result["session_ttl_seconds"])
    max_file = _int_from_merged_config(result["max_file_size_bytes"])
    default_chunk = _int_from_merged_config(result["default_chunk_size_bytes"])
    max_chunk = _int_from_merged_config(result["max_chunk_size_bytes"])
    if ttl < 60:
        raise RuntimeError("Invalid transfer configuration after merge")
    if max_file < 1 or default_chunk < 1 or max_chunk < 1:
        raise RuntimeError("Invalid transfer configuration after merge")
    if default_chunk > max_chunk:
        raise RuntimeError("Invalid transfer configuration after merge")
    return result


def get_transfer_store() -> TransferServerStore:
    global _CACHED_TRANSFER_STORE
    if _CACHED_TRANSFER_STORE is not None:
        return _CACHED_TRANSFER_STORE
    cfg = _merge_transfer_config()
    gzip_dir = cfg.get("server_gzip_download_buffer_dir")
    gzip_dir_str: str | None = str(gzip_dir) if gzip_dir is not None else None
    _CACHED_TRANSFER_STORE = TransferServerStore(
        storage_dir=str(cfg["storage_dir"]),
        session_ttl_seconds=_int_from_merged_config(cfg["session_ttl_seconds"]),
        max_file_size_bytes=_int_from_merged_config(cfg["max_file_size_bytes"]),
        default_chunk_size_bytes=_int_from_merged_config(
            cfg["default_chunk_size_bytes"]
        ),
        max_chunk_size_bytes=_int_from_merged_config(cfg["max_chunk_size_bytes"]),
        server_gzip_download_buffer_dir=gzip_dir_str,
    )
    return _CACHED_TRANSFER_STORE


def _validation_error_response(missing_fields: list[str]) -> JSONResponse:
    return JSONResponse(
        status_code=422,
        content={
            "success": False,
            "error_type": "InvalidRequest",
            "message": "Request validation failed",
            "details": {"missing_or_invalid_fields": missing_fields},
        },
    )


def _upload_session_not_found_response(transfer_id: str) -> JSONResponse:
    return JSONResponse(
        status_code=404,
        content={
            "success": False,
            "error_type": "TransferSessionNotFoundError",
            "message": "Upload session not found",
            "details": {"transfer_id": transfer_id},
        },
    )


def _download_session_not_found_response(transfer_id: str) -> JSONResponse:
    return JSONResponse(
        status_code=404,
        content={
            "success": False,
            "error_type": "TransferSessionNotFoundError",
            "message": "Download session not found",
            "details": {"transfer_id": transfer_id},
        },
    )


def _transfer_ws_progress_percent_from_session_upload(tid: str) -> int:
    """Best-effort upload progress percent from store; 0 on any failure."""
    try:
        s = get_transfer_store().get_session(tid.strip())
        return compute_transfer_progress_percent(
            int(s.offset), int(s.size_bytes), terminal=False
        )
    except Exception:
        return 0


def _session_progress_percent(transfer_id: str) -> int | None:
    try:
        s = get_transfer_store().get_session(transfer_id)
    except TransferError:
        return None
    return compute_transfer_progress_percent(
        int(s.offset), int(s.size_bytes), terminal=False
    )


async def _transfer_ws_should_emit_progress(transfer_id: str) -> bool:
    """Return True if at least MIN interval elapsed since last progress emit for this id."""
    tid = str(transfer_id).strip()
    if tid == "":
        return False
    now = time.monotonic()
    last = _transfer_ws_progress_last_mono.get(tid)
    if last is None or (now - last) >= _TRANSFER_WS_PROGRESS_MIN_INTERVAL_SEC:
        _transfer_ws_progress_last_mono[tid] = now
        return True
    return False


async def _notify_transfer_ws(
    transfer_id: str, event: str, payload: dict[str, object] | None = None
) -> None:
    try:
        await notify_transfer_state_changed(transfer_id, event, payload)
    except ValueError:
        pass
    except Exception as exc:
        get_global_logger().warning("Transfer WS notify failed: %s", exc)


async def _notify_transfer_progress_after_receipt(
    receipt: TransferReceipt, *, direction: str
) -> None:
    tid = str(receipt.transfer_id).strip()
    if not tid:
        return
    if not await _transfer_ws_should_emit_progress(tid):
        return
    off = int(receipt.offset)
    sz = int(receipt.size_bytes)
    st = str(receipt.status)
    pct = compute_transfer_progress_percent(off, sz, terminal=False)
    await _notify_transfer_ws(
        tid,
        EVENT_TRANSFER_PROGRESS,
        {
            "direction": direction,
            "offset": off,
            "size_bytes": sz,
            "progress_percent": pct,
            "status": st,
        },
    )


async def _notify_transfer_completed_upload(
    transfer_id: str, body: Mapping[str, object]
) -> None:
    tid = str(transfer_id).strip()
    if not tid:
        return
    await _notify_transfer_ws(
        tid,
        EVENT_TRANSFER_COMPLETED,
        {
            "direction": "upload",
            "status": str(body.get("status", "")),
            "filename": str(body.get("filename", "")),
            "checksum_algorithm": str(body.get("checksum_algorithm", "sha256")),
            "checksum_value": str(body.get("checksum_value", "")),
            "checksum_verified": True,
            "progress_percent": 100,
        },
    )


async def _notify_transfer_completed_download(
    transfer_id: str,
    filename: str,
    receipt: TransferDownloadReceipt,
) -> None:
    tid = str(transfer_id).strip()
    if not tid:
        return
    await _notify_transfer_ws(
        tid,
        EVENT_TRANSFER_COMPLETED,
        {
            "direction": "download",
            "status": str(receipt.status),
            "filename": filename,
            "checksum_algorithm": str(receipt.checksum_algorithm),
            "checksum_value": str(receipt.checksum_value),
            "checksum_verified": bool(receipt.checksum_verified),
            "progress_percent": 100,
        },
    )


async def _notify_transfer_failed(
    transfer_id: str,
    exc: BaseException,
    *,
    direction: str | None = None,
    progress_percent: int | None = None,
) -> None:
    payload: dict[str, object] = {
        "error_type": type(exc).__name__,
        "message": str(exc),
    }
    if isinstance(exc, TransferChecksumMismatchError):
        payload["error_type"] = "TransferChecksumMismatchError"
    if direction is not None:
        payload["direction"] = direction
    if progress_percent is not None:
        payload["progress_percent"] = progress_percent
    if isinstance(exc, TransferChecksumMismatchError):
        payload["checksum_algorithm"] = "sha256"
        if exc.checksum_expected is not None:
            payload["checksum_value"] = str(exc.checksum_expected)
    await _notify_transfer_ws(transfer_id, EVENT_TRANSFER_FAILED, payload)


async def _notify_transfer_expired(
    exc: TransferExpiredError, *, direction: str
) -> None:
    tid = exc.transfer_id
    if not tid:
        return
    payload = build_transfer_expired_ws_payload(
        transfer_id=str(tid),
        direction=direction,
        message=str(exc),
    )
    await _notify_transfer_ws(str(tid), EVENT_TRANSFER_EXPIRED, payload)


async def expire_transfer_sessions_and_notify(
    store: TransferServerStore | None = None,
    *,
    now_iso: str | None = None,
) -> int:
    """
    Run synchronous TTL expiry on the store, then push ``transfer_expired`` for each
    session newly marked expired. Uses ``notify_transfer_state_changed`` (respects
    ``transfer.ws_push_enabled`` and terminal dedup).
    """
    st = store or get_transfer_store()
    result = st.expire_stale_sessions(now_iso=now_iso)
    if not result.expired:
        return result.count
    await asyncio.gather(
        *[
            notify_transfer_state_changed(
                tid,
                EVENT_TRANSFER_EXPIRED,
                build_transfer_expired_ws_payload(
                    transfer_id=tid,
                    direction=direction,
                ),
            )
            for tid, direction in result.expired
        ]
    )
    return result.count


async def _poll_job_status(
    queue_manager: Any,
    job_id: str,
    command_name: str,
    poll_interval: float,
    max_wait_time: Optional[float],
    logger: Any,
) -> Dict[str, Any]:
    """
    Automatically poll job status until completion or timeout.

    This function uses asyncio.sleep (non-blocking) to poll job status
    at specified intervals. It works for all protocols (HTTP, HTTPS, mTLS)
    since it's server-side and doesn't depend on client protocol.

    Args:
        queue_manager: Queue manager instance
        job_id: Job identifier to poll
        command_name: Command name for logging
        poll_interval: Interval in seconds between status checks
        max_wait_time: Maximum time to wait in seconds (None = no limit)
        logger: Logger instance

    Returns:
        Final job result dictionary

    Raises:
        TimeoutError: If max_wait_time is exceeded
        InternalError: If job is not found or other error occurs
    """
    from mcp_proxy_adapter.integrations.queuemgr_integration import (
        QueueJobStatus,
        QueueJobError,
    )

    start_time = time.time()
    last_progress = -1
    last_status = None

    while True:
        # Check timeout
        elapsed = time.time() - start_time
        if max_wait_time is not None and elapsed >= max_wait_time:
            raise TimeoutError(
                f"Polling timeout after {max_wait_time} seconds for job {job_id}",
                data={
                    "job_id": job_id,
                    "command": command_name,
                    "elapsed": elapsed,
                    "max_wait_time": max_wait_time,
                },
            )

        try:
            # Get job status (non-blocking async call)
            job_result = await queue_manager.get_job_status(job_id)
        except QueueJobError as e:
            # Job not found or other queue error
            raise InternalError(
                f"Failed to get job status for {job_id}: {str(e)}",
                data={"job_id": job_id, "command": command_name},
            )

        current_status = job_result.status
        current_progress = job_result.progress

        # Log status changes and progress updates
        if current_status != last_status:
            logger.info(
                "Job %s (command=%s) status changed: %s -> %s",
                job_id,
                command_name,
                last_status or "unknown",
                current_status,
            )
            last_status = current_status

        if current_progress != last_progress:
            logger.debug(
                "Job %s (command=%s) progress: %d%% - %s",
                job_id,
                command_name,
                current_progress,
                job_result.description or "",
            )
            last_progress = current_progress

        # Check if job is in terminal state
        if current_status in (
            QueueJobStatus.COMPLETED,
            QueueJobStatus.FAILED,
            QueueJobStatus.STOPPED,
        ):
            # Return final result
            if current_status == QueueJobStatus.COMPLETED:
                # Return success result with job data
                return {
                    "success": True,
                    "job_id": job_id,
                    "status": current_status,
                    "result": job_result.result or {},
                    "progress": current_progress,
                    "description": job_result.description,
                    "message": f"Command '{command_name}' completed successfully",
                }
            elif current_status == QueueJobStatus.FAILED:
                # Return error result
                return {
                    "success": False,
                    "job_id": job_id,
                    "status": current_status,
                    "error": job_result.error or "Job failed",
                    "result": job_result.result or {},
                    "progress": current_progress,
                    "description": job_result.description,
                    "message": f"Command '{command_name}' failed",
                }
            else:  # STOPPED
                # Return stopped result
                return {
                    "success": False,
                    "job_id": job_id,
                    "status": current_status,
                    "result": job_result.result or {},
                    "progress": current_progress,
                    "description": job_result.description,
                    "message": f"Command '{command_name}' was stopped",
                }

        # Wait before next check (non-blocking async sleep)
        # This works for all protocols since it's server-side
        await asyncio.sleep(poll_interval)


async def execute_command(
    command_name: str,
    params: Optional[Dict[str, Any]],
    request_id: Optional[str] = None,
    request: Optional[Request] = None,
) -> Dict[str, Any]:
    """Execute a registered command by name with parameters.

    If command has use_queue=True, command will be executed via queue and job_id will be returned.
    Otherwise, command will be executed synchronously and result will be returned.

    Raises MethodNotFoundError if command is not found.
    Wraps unexpected exceptions into InternalError.
    """
    logger = RequestLogger(__name__, request_id) if request_id else get_global_logger()

    try:
        logger.info(f"Executing command: {command_name}")

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

        # Build context (user info if middleware set state)
        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 [],
                }

        # Check if command should be executed via queue
        use_queue = getattr(command_class, "use_queue", False)

        if use_queue:
            # Execute via queue - enqueue quickly and offload heavy work to worker process.
            # HTTP/JSON-RPC layer should only:
            #   1) validate params
            #   2) enqueue CommandExecutionJob
            #   3) return job_id to client as fast as possible (or poll if poll_interval specified)
            #
            # All heavy processing must happen inside the queue worker process so that
            # long-running pipelines do not block the initial HTTP request or hit
            # fixed 30s timeouts in queuemgr start_job / HTTP client.
            try:
                from mcp_proxy_adapter.integrations.queuemgr_integration import (
                    get_global_queue_manager,
                )
                from mcp_proxy_adapter.commands.queue.jobs import CommandExecutionJob
                from mcp_proxy_adapter.commands.hooks import hooks
                import uuid

                # Check for automatic polling parameters
                # First check command parameters, then fall back to config defaults
                poll_interval = params.get("poll_interval") if params else None
                max_wait_time = params.get("max_wait_time") if params else None

                # If not specified in params, try to get from config
                if poll_interval is None:
                    try:
                        from mcp_proxy_adapter.config import get_config

                        config = get_config()
                        if hasattr(config, "model") and hasattr(
                            config.model, "queue_manager"
                        ):
                            queue_config = config.model.queue_manager
                            poll_interval = queue_config.default_poll_interval
                    except Exception:
                        # Config might not be available, use default 0.0
                        poll_interval = 0.0

                if max_wait_time is None:
                    try:
                        from mcp_proxy_adapter.config import get_config

                        config = get_config()
                        if hasattr(config, "model") and hasattr(
                            config.model, "queue_manager"
                        ):
                            queue_config = config.model.queue_manager
                            max_wait_time = queue_config.default_max_wait_time
                    except Exception:
                        # Config might not be available, ignore
                        pass

                # Validate poll_interval: must be >= 0 (0 means no polling)
                if poll_interval is not None:
                    if not isinstance(poll_interval, (int, float)) or poll_interval < 0:
                        raise ValidationError(
                            "poll_interval must be >= 0 (0 means no polling, > 0 enables automatic polling)",
                            data={"poll_interval": poll_interval},
                        )

                # Validate max_wait_time if specified: must be > 0
                if max_wait_time is not None:
                    if (
                        not isinstance(max_wait_time, (int, float))
                        or max_wait_time <= 0
                    ):
                        raise ValidationError(
                            "max_wait_time must be a positive number > 0",
                            data={"max_wait_time": max_wait_time},
                        )

                queue_manager = await get_global_queue_manager()
                job_id = str(uuid.uuid4())

                # Get list of modules to import in child process
                # This ensures commands are available in spawn mode
                auto_import_modules = hooks.get_auto_import_modules()

                # Prepare job parameters - remove poll_interval and max_wait_time before queuing
                # These are handler-level parameters, not command parameters
                command_params = (params or {}).copy()
                command_params.pop("poll_interval", None)
                command_params.pop("max_wait_time", None)

                # Prepare job parameters - heavy processing happens in CommandExecutionJob.run()
                job_params = {
                    "command": command_name,
                    "params": command_params,
                    "context": context,
                    "auto_import_modules": auto_import_modules,  # Pass modules to child process
                }

                # Add job to queue (fast, no heavy work here)
                await queue_manager.add_job(CommandExecutionJob, job_id, job_params)

                # Start job in background to avoid blocking HTTP request on queuemgr timeouts
                async def _start_job_background() -> None:
                    try:
                        await queue_manager.start_job(job_id)
                        logger.info(
                            "Background start for job %s (command=%s) completed",
                            job_id,
                            command_name,
                        )
                    except Exception as start_exc:
                        # Important: do not fail the original HTTP request if queuemgr
                        # reports a timeout or IPC error. The job may still transition
                        # to a terminal state and be observable via queue_get_job_status.
                        logger.warning(
                            "Background start for job %s (command=%s) failed: %s",
                            job_id,
                            command_name,
                            start_exc,
                        )

                asyncio.create_task(_start_job_background())

                # If poll_interval > 0, automatically poll for job completion
                # If poll_interval == 0 or None, return job_id immediately (no polling)
                if poll_interval is not None and poll_interval > 0:
                    return await _poll_job_status(
                        queue_manager=queue_manager,
                        job_id=job_id,
                        command_name=command_name,
                        poll_interval=poll_interval,
                        max_wait_time=max_wait_time,
                        logger=logger,
                    )

                # Return job_id immediately (always returned, even with polling).
                # If poll_interval > 0, additional data (result, status, progress) will be included.
                # Client code can use job_id to track progress or get status later.
                return {
                    "success": True,
                    "job_id": job_id,
                    "status": "pending",
                    "message": f"Command '{command_name}' has been queued for execution",
                }

            except MicroserviceError:
                # Re-raise domain-specific errors (ValidationError, etc.) so that JSON-RPC layer can format them properly
                raise
            except Exception as exc:
                logger.exception(f"Failed to queue command '{command_name}': {exc}")
                raise InternalError(f"Failed to queue command: {str(exc)}")

        # Execute synchronously (default behavior)
        # Ensure params is a dict, not None
        if params is None:
            params = {}
        started_at = time.time()
        effective_timeout = resolve_timeout(SYNC_COMMAND_TIMEOUT)
        try:
            result_obj = await asyncio.wait_for(
                command_class.run(**params, context=context),
                timeout=effective_timeout,
            )
        except asyncio.TimeoutError:
            elapsed = time.time() - started_at
            raise InternalError(f"Command timed out after {elapsed:.2f}s")

        elapsed = time.time() - started_at
        logger.info(f"Command '{command_name}' executed in {elapsed:.3f}s")
        return result_obj.to_dict()

    except MicroserviceError:
        # Re-raise domain-specific errors so that JSON-RPC layer can format them properly.
        raise
    except Exception as exc:
        logger.exception(f"Unhandled error in command '{command_name}': {exc}")
        raise InternalError("Internal error", data={"error": str(exc)})


async def handle_batch_json_rpc(
    batch_requests: List[Dict[str, Any]], request: Optional[Request] = None
) -> List[Dict[str, Any]]:
    """Handle batch JSON-RPC requests."""
    responses: List[Dict[str, Any]] = []
    request_id = getattr(request.state, "request_id", None) if request else None
    for item in batch_requests:
        responses.append(await handle_json_rpc(item, request_id, request))
    return responses


async def handle_json_rpc(
    request_data: Dict[str, Any],
    request_id: Optional[str] = None,
    request: Optional[Request] = None,
) -> Dict[str, Any]:
    """Handle a single JSON-RPC request with strict 2.0 compatibility.

    Also supports simplified form: {"command": "echo", "params": {...}}.
    """
    # Keep handler logic very thin – all heavy work is delegated to execute_command().
    method: Optional[str]
    params: Dict[str, Any]
    json_rpc_id: Any

    if "jsonrpc" in request_data:
        if request_data.get("jsonrpc") != "2.0":
            return _error_response(
                InvalidRequestError("Invalid Request: jsonrpc must be '2.0'"),
                request_data.get("id"),
            )
        method = request_data.get("method")
        params = request_data.get("params") or {}
        json_rpc_id = request_data.get("id")
        if not method:
            return _error_response(
                InvalidRequestError("Invalid Request: method is required"), json_rpc_id
            )
    else:
        # Simplified
        method = request_data.get("command")
        params = request_data.get("params") or {}
        json_rpc_id = request_data.get("id", 1)
        if not method:
            return _error_response(
                InvalidRequestError("Invalid Request: command is required"), json_rpc_id
            )

    # Compatibility: some clients send job_id at request top level instead of in params.
    # Ensure job status commands receive job_id so backend validation does not fail.
    if method in ("embed_job_status", "queue_get_job_status"):
        if not params.get("job_id") and request_data.get("job_id"):
            params = {**params, "job_id": request_data["job_id"]}
        if not params.get("job_id"):
            return _error_response(
                InvalidRequestError(
                    'Parameter \'job_id\' is required. Send params: {"job_id": "<job_id from chunk response>"}'
                ),
                json_rpc_id,
            )

    if method in (
        "transfer_ack",
        "job_input_accepted",
        "job_processing_step",
        "artifact_ready",
    ):
        if not isinstance(params, dict):
            params = {}
        from mcp_proxy_adapter.api import jsonrpc_job_lifecycle as _jl
        from mcp_proxy_adapter.core.errors import NotFoundError
        from mcp_proxy_adapter.transfer import (
            TransferSessionNotFoundError as _TSNF,
        )

        try:
            if method == "transfer_ack":
                result = await _jl.jsonrpc_transfer_ack(params)
            elif method == "job_input_accepted":
                result = await _jl.jsonrpc_job_input_accepted(params)
            elif method == "job_processing_step":
                result = await _jl.jsonrpc_job_processing_step(params)
            else:
                result = await _jl.jsonrpc_artifact_ready(params)
            return {"jsonrpc": "2.0", "result": result, "id": json_rpc_id}
        except ValidationError as exc:
            return _error_response(exc, json_rpc_id)
        except _TSNF as exc:
            return _error_response(
                NotFoundError(
                    str(exc),
                    data=exc.to_dict(),
                ),
                json_rpc_id,
            )
        except (TransferAckError, TransferChecksumMismatchError) as exc:
            return _error_response(
                InvalidRequestError(str(exc), data=exc.to_dict()),
                json_rpc_id,
            )

    if method == "execute_async":
        command = params.get("command")
        if not isinstance(command, str) or not command.strip():
            return _error_response(
                InvalidRequestError("execute_async requires 'command' in params"),
                json_rpc_id,
            )
        command_params = params.get("params")
        if not isinstance(command_params, dict):
            command_params = {}
        result = await execute_command_async(
            command, command_params, None, request_id, request
        )
        return {"jsonrpc": "2.0", "result": result, "id": json_rpc_id}

    result = await execute_command(method, params, request_id, request)
    return {"jsonrpc": "2.0", "result": result, "id": json_rpc_id}


def _error_response(error: MicroserviceError, request_id: Any) -> Dict[str, Any]:
    """
    Create JSON-RPC error response.

    Args:
        error: Microservice error instance
        request_id: Request ID from original request

    Returns:
        Dictionary with JSON-RPC error response format
    """
    return {"jsonrpc": "2.0", "error": error.to_dict(), "id": request_id}


async def get_server_health() -> Dict[str, Any]:
    """Return server health info.

    Server-reported identity (instance_uuid) is taken only from config
    (registration.instance_uuid). No runtime-generated IDs are exposed.
    """
    import os
    import platform
    import sys
    import psutil  # type: ignore[import-untyped]
    from datetime import datetime

    process = psutil.Process(os.getpid())
    start_time = datetime.fromtimestamp(process.create_time())
    uptime_seconds = (datetime.now() - start_time).total_seconds()
    mem = process.memory_info().rss / (1024 * 1024)

    from mcp_proxy_adapter.core.proxy_registration import get_proxy_registration_status

    result: Dict[str, Any] = {
        "status": "ok",
        "model": "mcp-proxy-adapter",
        "version": "1.0.0",
        "uptime": uptime_seconds,
        "components": {
            "system": {
                "python_version": sys.version,
                "platform": platform.platform(),
                "cpu_count": os.cpu_count(),
            },
            "process": {
                "pid": os.getpid(),
                "memory_usage_mb": mem,
                "start_time": start_time.isoformat(),
            },
            "commands": {"registered_count": len(registry.get_all_commands())},
            "proxy_registration": get_proxy_registration_status(),
        },
    }
    # Expose server identifier only from config (registration.instance_uuid).
    try:
        from mcp_proxy_adapter.config import get_config

        cfg = get_config()
        if hasattr(cfg, "model") and hasattr(cfg.model, "registration"):
            reg = cfg.model.registration
            if hasattr(reg, "instance_uuid") and reg.instance_uuid:
                result["instance_uuid"] = reg.instance_uuid
    except Exception:
        pass
    return result


async def get_commands_list() -> Dict[str, Any]:
    """Return list of registered commands with schemas (CommandListResponse)."""
    result: Dict[str, Dict[str, Any]] = {}
    for name, cls in registry.get_all_commands().items():
        try:
            schema = cls.get_schema() if hasattr(cls, "get_schema") else {}
        except Exception:
            schema = {}
        result[name] = {
            "name": name,
            "schema": schema,
            "description": (
                schema.get("description", "") if isinstance(schema, dict) else ""
            ),
        }
    return {"commands": result}


async def handle_heartbeat() -> Dict[str, Any]:
    """Handle heartbeat request from proxy.

    This endpoint is used by the proxy to check if the server is alive.
    Returns server status and metadata.
    """
    logger = get_global_logger()
    logger.debug("💓 Heartbeat request received")

    # Get server info from config if available
    server_name = "mcp-proxy-adapter"
    server_url = "http://localhost:8080"

    try:
        from mcp_proxy_adapter.config import get_config

        cfg = get_config()
        if hasattr(cfg, "model") and hasattr(cfg.model, "server"):
            server_config = cfg.model.server
            if hasattr(server_config, "name"):
                server_name = server_config.name or server_name
            if hasattr(server_config, "host") and hasattr(server_config, "port"):
                protocol = (
                    "https"
                    if (
                        hasattr(server_config, "ssl")
                        and server_config.ssl
                        and server_config.ssl.enabled
                    )
                    else "http"
                )
                host = server_config.host or "localhost"
                port = server_config.port or 8080
                server_url = f"{protocol}://{host}:{port}"
    except Exception:
        pass  # Use defaults if config is not available

    return {
        "status": "ok",
        "server_name": server_name,
        "server_url": server_url,
        "timestamp": time.time(),
    }


async def create_upload_session(payload: dict[str, object]) -> JSONResponse:
    try:
        body = await run_create_upload_session(payload)
    except TransferPayloadValidationError as exc:
        return _validation_error_response(exc.missing_fields)
    except (TransferTooLargeError, TransferCompressionError, TransferError) as exc:
        status, out = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=out)
    tid = str(body.get("transfer_id", "")).strip()
    if tid:
        await _notify_transfer_ws(
            tid,
            EVENT_TRANSFER_ACCEPTED,
            {
                "direction": "upload",
                "status": str(body.get("status", "")),
                "filename": str(body.get("filename", "")),
            },
        )
    return JSONResponse(status_code=201, content=body)


async def get_upload_status(transfer_id: str) -> JSONResponse:
    try:
        body = await run_get_upload_session(transfer_id)
    except TransferExpiredError as exc:
        await _notify_transfer_expired(exc, direction="upload")
        status, out = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=out)
    except TransferSessionNotFoundError as exc:
        status, out = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=out)
    except TransferSessionKindMismatch as exc:
        return _upload_session_not_found_response(exc.transfer_id)
    return JSONResponse(status_code=200, content=body)


async def upload_chunk(transfer_id: str, offset: int, request: Request) -> JSONResponse:
    chunk_bytes: bytes = await request.body()
    if offset < 0:
        return _validation_error_response(["offset"])
    try:
        store = get_transfer_store()
        session = store.get_session(transfer_id)
        if str(session.direction) != "upload":
            return _upload_session_not_found_response(transfer_id)
        receipt = store.write_upload_chunk(transfer_id, offset, chunk_bytes)
    except (
        TransferResumeConflictError,
        TransferTooLargeError,
        TransferSessionNotFoundError,
        TransferExpiredError,
    ) as exc:
        if isinstance(exc, TransferExpiredError):
            await _notify_transfer_expired(exc, direction="upload")
        elif isinstance(exc, (TransferResumeConflictError, TransferTooLargeError)):
            tid = str(exc.transfer_id) if exc.transfer_id else transfer_id
            pct = _session_progress_percent(tid)
            await _notify_transfer_failed(
                tid,
                exc,
                direction="upload",
                progress_percent=0 if pct is None else pct,
            )
        status, body = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=body)
    await _notify_transfer_progress_after_receipt(receipt, direction="upload")
    return JSONResponse(status_code=200, content=receipt.to_dict())


async def complete_upload_session(transfer_id: str) -> JSONResponse:
    try:
        body = await run_complete_upload_session(transfer_id)
    except TransferExpiredError as exc:
        status, out = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=out)
    except TransferSessionNotFoundError as exc:
        status, out = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=out)
    except TransferSessionKindMismatch as exc:
        return _upload_session_not_found_response(exc.transfer_id)
    except (
        TransferChecksumMismatchError,
        TransferCompressionError,
        TransferError,
    ) as exc:
        status, out = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=out)
    return JSONResponse(status_code=200, content=body)


async def create_download_session(payload: dict[str, object]) -> JSONResponse:
    try:
        body = await run_create_download_session(payload)
    except TransferPayloadValidationError as exc:
        return _validation_error_response(exc.missing_fields)
    except (TransferCompressionError, TransferTooLargeError, TransferError) as exc:
        status, out = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=out)
    tid = str(body.get("transfer_id", "")).strip()
    if tid:
        await _notify_transfer_ws(
            tid,
            EVENT_TRANSFER_ACCEPTED,
            {
                "direction": "download",
                "status": str(body.get("status", "")),
                "filename": str(body.get("filename", "")),
            },
        )
    return JSONResponse(status_code=201, content=body)


async def get_download_status(transfer_id: str) -> JSONResponse:
    try:
        body = await run_get_download_session(transfer_id)
    except TransferExpiredError as exc:
        await _notify_transfer_expired(exc, direction="download")
        status, out = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=out)
    except TransferSessionNotFoundError as exc:
        status, out = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=out)
    except TransferSessionKindMismatch as exc:
        return _download_session_not_found_response(exc.transfer_id)
    return JSONResponse(status_code=200, content=body)


async def download_chunk(
    transfer_id: str,
    offset: int = Query(..., description="Byte offset"),
    limit: int = Query(..., description="Maximum bytes to read"),
) -> Response | JSONResponse:
    if offset < 0:
        return _validation_error_response(["offset"])
    if limit < 1:
        return _validation_error_response(["limit"])
    cfg = _merge_transfer_config()
    max_chunk = _int_from_merged_config(cfg["max_chunk_size_bytes"])
    if limit > max_chunk:
        limit = max_chunk
    try:
        store = get_transfer_store()
        session = store.get_session(transfer_id)
    except (TransferSessionNotFoundError, TransferExpiredError) as exc:
        if isinstance(exc, TransferExpiredError):
            await _notify_transfer_expired(exc, direction="download")
        status, body = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=body)
    if str(session.direction) != "download":
        return _download_session_not_found_response(transfer_id)
    try:
        chunk_bytes, dl_receipt = store.read_download_chunk(transfer_id, offset, limit)
    except (
        TransferResumeConflictError,
        TransferRangeNotSatisfiableError,
        TransferSessionNotFoundError,
        TransferExpiredError,
    ) as exc:
        if isinstance(exc, TransferExpiredError):
            await _notify_transfer_expired(exc, direction="download")
        elif isinstance(
            exc, (TransferResumeConflictError, TransferRangeNotSatisfiableError)
        ):
            tid = str(exc.transfer_id) if exc.transfer_id else transfer_id
            pct = _session_progress_percent(tid)
            await _notify_transfer_failed(
                tid,
                exc,
                direction="download",
                progress_percent=0 if pct is None else pct,
            )
        status, body = build_transfer_error_response(exc)
        return JSONResponse(status_code=status, content=body)
    tid = str(transfer_id).strip()
    if tid:
        progress_size = (
            int(session.size_bytes)
            if str(session.compression) == "gzip"
            else (
                int(session.plaintext_size_bytes)
                if session.plaintext_size_bytes is not None
                else int(session.size_bytes)
            )
        )
        off = int(dl_receipt.offset)
        term = bool(dl_receipt.completed)
        pct = compute_transfer_progress_percent(off, progress_size, terminal=term)
        if term:
            await _notify_transfer_ws(
                tid,
                EVENT_TRANSFER_PROGRESS,
                {
                    "direction": "download",
                    "offset": off,
                    "size_bytes": progress_size,
                    "progress_percent": pct,
                    "status": str(dl_receipt.status),
                },
            )
        elif await _transfer_ws_should_emit_progress(tid):
            await _notify_transfer_ws(
                tid,
                EVENT_TRANSFER_PROGRESS,
                {
                    "direction": "download",
                    "offset": off,
                    "size_bytes": progress_size,
                    "progress_percent": pct,
                    "status": str(dl_receipt.status),
                },
            )
        if term:
            await _notify_transfer_completed_download(
                tid, str(session.filename), dl_receipt
            )
    hdrs: dict[str, str] = {
        "X-Transfer-Id": transfer_id,
        "X-Transfer-Offset": str(dl_receipt.offset),
        "X-Transfer-Completed": "true" if dl_receipt.completed else "false",
        "X-Transfer-Status": str(dl_receipt.status),
        "X-Transfer-Wire-Size-Bytes": str(session.size_bytes),
    }
    if session.plaintext_size_bytes is not None:
        hdrs["X-Transfer-Plaintext-Size-Bytes"] = str(session.plaintext_size_bytes)
    return Response(
        content=chunk_bytes,
        media_type="application/octet-stream",
        headers=hdrs,
    )
