"""OpenAPI tool executor -- makes the actual HTTP calls.

Given an ApiOperation and user-provided arguments, this module:
  1. Builds the full URL (base + path with path params substituted)
  2. Separates args into path, query, header, and body params
  3. Applies authentication (API key, Bearer, Basic)
  4. Makes the HTTP request via httpx
  5. Returns the response in a structured format
"""

from __future__ import annotations

import base64
import json
import logging
import time
from typing import Any

import httpx

from mcp_bridge.openapi.models import (
    ApiOperation,
    ApiSpec,
    AuthScheme,
    AuthSchemeType,
    ParameterLocation,
)

logger = logging.getLogger(__name__)


class ApiToolExecutor:
    """Execute API operations as MCP tool calls.

    Takes the parsed spec and handles the HTTP mechanics:
    argument routing, auth injection, request building, and
    response formatting.
    """

    def __init__(self, spec: ApiSpec) -> None:
        self._spec = spec
        self._client: httpx.AsyncClient | None = None
        self._auth_values: dict[str, str] = {}  # scheme_name → credential value

    # ------------------------------------------------------------------
    # Auth configuration
    # ------------------------------------------------------------------

    def configure_auth(self, scheme_name: str, value: str) -> None:
        """Set the credential value for an auth scheme.

        Args:
            scheme_name: Name of the security scheme from the spec.
            value: The credential (API key, Bearer token, username:password).
        """
        self._auth_values[scheme_name] = value
        logger.info("Auth configured: scheme=%s", scheme_name)

    def configure_all_auth(self, credentials: dict[str, str]) -> None:
        """Set credentials for multiple auth schemes at once."""
        self._auth_values.update(credentials)

    # ------------------------------------------------------------------
    # Tool execution
    # ------------------------------------------------------------------

    async def execute(
        self,
        operation_id: str,
        arguments: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Execute an API operation with the given arguments.

        Args:
            operation_id: The operation_id (or effective_tool_name) to execute.
            arguments: User-provided arguments matching the tool's input schema.

        Returns:
            Dict with: status_code, headers (subset), body, success, latency_ms.
        """
        # Find the operation
        op = self._find_operation(operation_id)
        if not op:
            return {"error": f"Operation '{operation_id}' not found", "success": False}

        args = arguments or {}
        start = time.time()

        try:
            # Build the request
            url = self._build_url(op, args)
            headers = self._build_headers(op, args)
            query_params = self._build_query_params(op, args)
            body = self._build_body(op, args)

            # Apply auth
            self._apply_auth(op, headers, query_params)

            # Execute
            client = self._get_client()
            response = await client.request(
                method=op.method.upper(),
                url=url,
                headers=headers,
                params=query_params if query_params else None,
                json=body if body and op.request_body and "json" in op.request_body.content_type else None,
                data=body if body and op.request_body and "form" in (op.request_body.content_type or "") else None,
            )

            latency_ms = (time.time() - start) * 1000

            # Parse response
            try:
                resp_body = response.json()
            except (json.JSONDecodeError, ValueError):
                resp_body = response.text

            result = {
                "success": response.is_success,
                "status_code": response.status_code,
                "body": resp_body,
                "latency_ms": round(latency_ms, 1),
            }

            logger.info(
                "API call: %s %s → %d (%.0fms)",
                op.method.upper(),
                op.path,
                response.status_code,
                latency_ms,
            )

            return result

        except httpx.HTTPError as e:
            latency_ms = (time.time() - start) * 1000
            logger.error("API call failed: %s %s → %s", op.method.upper(), op.path, e)
            return {
                "success": False,
                "error": str(e),
                "latency_ms": round(latency_ms, 1),
            }

    # ------------------------------------------------------------------
    # Request building
    # ------------------------------------------------------------------

    def _build_url(self, op: ApiOperation, args: dict[str, Any]) -> str:
        """Build the full URL with path parameters substituted."""
        path = op.path
        for param in op.parameters:
            if param.location == ParameterLocation.PATH:
                value = args.get(param.name, "")
                path = path.replace(f"{{{param.name}}}", str(value))
        return f"{self._spec.base_url}{path}"

    def _build_headers(self, op: ApiOperation, args: dict[str, Any]) -> dict[str, str]:
        """Extract header parameters from arguments."""
        headers: dict[str, str] = {"Accept": "application/json"}
        for param in op.parameters:
            if param.location == ParameterLocation.HEADER and param.name in args:
                headers[param.name] = str(args[param.name])
        return headers

    def _build_query_params(self, op: ApiOperation, args: dict[str, Any]) -> dict[str, str]:
        """Extract query parameters from arguments."""
        params: dict[str, str] = {}
        for param in op.parameters:
            if param.location == ParameterLocation.QUERY and param.name in args:
                params[param.name] = str(args[param.name])
        return params

    def _build_body(self, op: ApiOperation, args: dict[str, Any]) -> dict[str, Any] | None:
        """Extract request body from arguments."""
        if not op.request_body:
            return None

        body: dict[str, Any] = {}
        # Body properties are flattened into the arguments
        param_names = {p.name for p in op.parameters}
        for key, value in args.items():
            if key not in param_names:
                body[key] = value

        return body if body else None

    # ------------------------------------------------------------------
    # Auth injection
    # ------------------------------------------------------------------

    def _apply_auth(
        self,
        op: ApiOperation,
        headers: dict[str, str],
        query_params: dict[str, str],
    ) -> None:
        """Apply authentication to the request."""
        for scheme in self._spec.auth_schemes:
            value = self._auth_values.get(scheme.name)
            if not value:
                continue

            if scheme.type == AuthSchemeType.BEARER:
                headers["Authorization"] = f"Bearer {value}"

            elif scheme.type == AuthSchemeType.API_KEY:
                if scheme.api_key_location == "header" and scheme.api_key_name:
                    headers[scheme.api_key_name] = value
                elif scheme.api_key_location == "query" and scheme.api_key_name:
                    query_params[scheme.api_key_name] = value

            elif scheme.type == AuthSchemeType.BASIC:
                # value should be "username:password"
                encoded = base64.b64encode(value.encode()).decode()
                headers["Authorization"] = f"Basic {encoded}"

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _find_operation(self, operation_id: str) -> ApiOperation | None:
        """Find an operation by its operation_id or effective_tool_name."""
        for op in self._spec.operations:
            if op.operation_id == operation_id or op.effective_tool_name == operation_id:
                return op
        return None

    def _get_client(self) -> httpx.AsyncClient:
        """Get or create the HTTP client."""
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(
                timeout=60.0,
                follow_redirects=True,
                limits=httpx.Limits(max_connections=20, max_keepalive_connections=5),
            )
        return self._client

    async def close(self) -> None:
        """Close the HTTP client."""
        if self._client and not self._client.is_closed:
            await self._client.aclose()
            self._client = None
