"""RemoteToolExecutor — executes tools on the qRemoteX agent.

Handles two execution kinds per Section 12.3:
  NATIVE:      Built-in Python handlers (system.db_query, system.http_request, etc.)
  REMOTE_HTTP: HTTP calls to endpoints reachable from this machine's network.

Tool definitions (including kind, schema, http_config) are sent from qRunX
in the QRP execute_tool message.  All secrets/templates are PRE-RESOLVED by
qRunX before dispatch — qRemoteX is a "dumb executor".

Architecture reference: Section 12.3 — Tool Execution Architecture on qRemoteX.
"""

from __future__ import annotations

import asyncio
import logging
import time
from typing import Any

import aiohttp

from qremotex.config import ExecutorConfig
from qremotex.tools.registry import ToolRegistry, build_default_registry

logger = logging.getLogger("qremotex.execution.tool_executor")


class RemoteToolExecutor:
    """Executes tools dispatched from qRunX via QRP execute_tool messages.

    Supports two kinds:
    - NATIVE:      Invokes a registered Python handler locally.
    - REMOTE_HTTP: Makes an HTTP call from this machine's network.
    """

    def __init__(self, config: ExecutorConfig) -> None:
        self._config = config
        self._registry: ToolRegistry = build_default_registry()

    # ------------------------------------------------------------------ #
    # Public API
    # ------------------------------------------------------------------ #

    async def execute(
        self,
        tool_key: str,
        tool_params: dict[str, Any],
        tool_definition: dict[str, Any],
        context: dict[str, Any],
    ) -> dict[str, Any]:
        """Execute a tool and return a result dict.

        Args:
            tool_key: Full tool key (e.g. "system.db_query", "project.crm_api")
            tool_params: Fully-resolved input parameters from qRunX
            tool_definition: Tool metadata including execution_target_kind,
                             input_schema, http_config, timeout_seconds
            context: Execution context (run_id, request_id, subscription_id, trace_id)

        Returns:
            dict: {"success": True, "result": ..., "duration_ms": ...}
                  or {"success": False, "error": "..."}
        """
        # Check allowlist/blocklist
        rejection = self._check_tool_allowed(tool_key)
        if rejection:
            return {"success": False, "error": rejection}

        kind = tool_definition.get("execution_target_kind", "NATIVE").upper()
        timeout_sec = tool_definition.get("timeout_seconds", self._config.exec_timeout_sec)

        start = time.monotonic()
        try:
            if kind == "NATIVE":
                result = await asyncio.wait_for(
                    self._execute_native(tool_key, tool_params, context),
                    timeout=timeout_sec,
                )
            elif kind == "REMOTE_HTTP":
                result = await asyncio.wait_for(
                    self._execute_http(tool_key, tool_params, tool_definition, context),
                    timeout=timeout_sec,
                )
            else:
                return {
                    "success": False,
                    "error": f"Unsupported execution_target_kind: {kind}",
                }

            duration_ms = int((time.monotonic() - start) * 1000)

            # If the handler already returned a structured result, use it
            if isinstance(result, dict) and "error" in result and "success" not in result:
                return {"success": False, "error": result["error"], "duration_ms": duration_ms}

            return {"success": True, "result": result, "duration_ms": duration_ms}

        except asyncio.TimeoutError:
            duration_ms = int((time.monotonic() - start) * 1000)
            logger.warning("Tool %s timed out after %ds", tool_key, timeout_sec)
            return {
                "success": False,
                "error": f"Tool execution timed out after {timeout_sec}s",
                "duration_ms": duration_ms,
            }
        except Exception as e:
            duration_ms = int((time.monotonic() - start) * 1000)
            logger.error("Tool %s failed: %s: %s", tool_key, type(e).__name__, e)
            return {
                "success": False,
                "error": f"{type(e).__name__}: {e}",
                "duration_ms": duration_ms,
            }

    # ------------------------------------------------------------------ #
    # Allowlist / Blocklist (C7)
    # ------------------------------------------------------------------ #

    def _check_tool_allowed(self, tool_key: str) -> str | None:
        """Return an error string if the tool is blocked, None if allowed."""
        # Blocklist takes precedence
        if tool_key in self._config.blocked_tool_keys:
            return f"Tool '{tool_key}' is blocked by executor configuration"

        # Check system/user tool category
        is_system = tool_key.startswith("system.")
        if is_system and not self._config.enable_system_tools:
            return f"System tools disabled on this executor (tool: {tool_key})"
        if not is_system and not self._config.enable_user_tools:
            return f"User/project tools disabled on this executor (tool: {tool_key})"

        # Allowlist: "*" means all allowed, otherwise must be in the list
        allowed = self._config.allowed_tool_keys
        if allowed != "*" and isinstance(allowed, list):
            if tool_key not in allowed:
                return f"Tool '{tool_key}' not in executor allowlist"

        return None

    # ------------------------------------------------------------------ #
    # NATIVE execution
    # ------------------------------------------------------------------ #

    async def _execute_native(
        self,
        tool_key: str,
        tool_params: dict[str, Any],
        context: dict[str, Any],
    ) -> Any:
        """Execute a NATIVE tool via the registry."""
        if not self._registry.has(tool_key):
            available = self._registry.registered_keys
            raise ValueError(
                f"No native handler for '{tool_key}'. Available: {available}"
            )

        handler = self._registry.get(tool_key)
        logger.info("Executing NATIVE tool: %s", tool_key)
        return await handler(tool_params, context)

    # ------------------------------------------------------------------ #
    # REMOTE_HTTP execution
    # ------------------------------------------------------------------ #

    async def _execute_http(
        self,
        tool_key: str,
        tool_params: dict[str, Any],
        tool_definition: dict[str, Any],
        context: dict[str, Any],
    ) -> dict[str, Any]:
        """Execute a REMOTE_HTTP tool by making an HTTP call from this machine.

        http_config is pre-resolved by qRunX (secrets injected, templates expanded).
        """
        http_config = tool_definition.get("http_config")
        if not http_config:
            raise ValueError(f"REMOTE_HTTP tool '{tool_key}' missing http_config")

        url = http_config.get("url")
        if not url:
            raise ValueError(f"REMOTE_HTTP tool '{tool_key}' missing http_config.url")

        method = http_config.get("method", "POST").upper()
        headers = dict(http_config.get("headers", {}))
        timeout_sec = tool_definition.get("timeout_seconds", 30)
        body_mode = http_config.get("body_mode", "json")  # json | form | raw

        logger.info("Executing REMOTE_HTTP tool: %s  %s %s", tool_key, method, url)

        timeout = aiohttp.ClientTimeout(total=timeout_sec)
        async with aiohttp.ClientSession(timeout=timeout) as session:
            kwargs: dict[str, Any] = {"headers": headers}

            if method in ("POST", "PUT", "PATCH") and tool_params:
                if body_mode == "form":
                    kwargs["data"] = tool_params
                elif body_mode == "raw":
                    kwargs["data"] = str(tool_params.get("body", ""))
                else:
                    kwargs["json"] = tool_params

            async with session.request(method, url, **kwargs) as resp:
                content_type = resp.headers.get("content-type", "")
                if "application/json" in content_type:
                    body = await resp.json()
                else:
                    body = await resp.text()

                return {
                    "status_code": resp.status,
                    "body": body,
                    "headers": {k: v for k, v in resp.headers.items()},
                }
