from __future__ import annotations

"""
Copyright (C) 2025 Luciano Guerche

This file is part of rabbitmq-mcp-server.

rabbitmq-mcp-server is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

rabbitmq-mcp-server is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with rabbitmq-mcp-server. If not, see <https://www.gnu.org/licenses/>.
"""

"""Operation executor for RabbitMQ Management API operations."""

import time
import uuid
from typing import Any
from urllib.parse import quote

import httpx
import structlog
from pydantic import BaseModel

from rabbitmq_mcp_connection.http_client import HTTPClient

logger = structlog.get_logger(__name__)


class ExecutionResult(BaseModel):
    """Result of operation execution.

    Implements AC #5 response format requirements.
    """

    status: str
    result: dict[str, Any]
    correlation_id: str
    metadata: dict[str, Any]


class ErrorResponse(BaseModel):
    """Structured error response.

    Implements AC #6 error handling requirements.
    """

    error_code: str
    message: str
    context: dict[str, Any]
    correlation_id: str
    timestamp: str


class OperationExecutor:
    """Execute RabbitMQ Management API operations with validation and error handling.

    Implements AC #2, #3, #5 requirements for operation execution:
    - Parameter validation with Pydantic schemas
    - HTTP request construction and execution
    - Response handling and formatting
    """

    def __init__(
        self,
        http_client: HTTPClient,
        operation_registry: dict[str, Any],
    ) -> None:
        """Initialize operation executor.

        Args:
            http_client: HTTPClient instance for executing requests
            operation_registry: Operations registry loaded from operations.json
        """
        self.http_client = http_client
        self.operation_registry = operation_registry

    def _build_url(
        self,
        url_path_template: str,
        parameters: dict[str, Any],
    ) -> str:
        """Build URL from template and parameters with URL encoding.

        Implements AC #3 URL construction requirements:
        - Replace path parameters ({vhost}, {name}, etc.)
        - URL encode special characters (/ → %2F, space → %20)
        - Append query parameters for GET requests

        Args:
            url_path_template: URL path with {param} placeholders
            parameters: Parameter values to substitute

        Returns:
            Complete URL path with parameters substituted and encoded

        Raises:
            ValidationError: Required path parameter missing
        """
        url_path = url_path_template
        query_params: dict[str, Any] = {}

        # Replace path parameters
        import re

        path_param_pattern = r"\{([^}]+)\}"
        path_params = re.findall(path_param_pattern, url_path_template)

        for param_name in path_params:
            if param_name not in parameters:
                raise ValueError(f"Required path parameter '{param_name}' not provided")

            # URL encode the parameter value
            param_value = str(parameters[param_name])
            encoded_value = quote(param_value, safe="")
            url_path = url_path.replace(f"{{{param_name}}}", encoded_value)

        # Collect query parameters (parameters not used in path)
        for key, value in parameters.items():
            if key not in path_params:
                query_params[key] = value

        # Append query parameters for GET requests
        if query_params:
            query_string = "&".join(
                f"{key}={quote(str(value), safe='')}"
                for key, value in query_params.items()
            )
            url_path = f"{url_path}?{query_string}"

        return url_path

    def _build_request_body(
        self,
        operation: dict[str, Any],
        parameters: dict[str, Any],
    ) -> dict[str, Any] | None:
        """Build request body from parameters for POST/PUT/PATCH requests.

        Implements AC #3 request body construction requirements:
        - Extract body parameters (not path or query parameters)
        - Validate against request_schema if available
        - Serialize to JSON-compatible dict

        Args:
            operation: Operation metadata from registry
            parameters: All parameters provided by user

        Returns:
            Request body dict for POST/PUT/PATCH, None for GET/DELETE
        """
        http_method = operation.get("http_method", "GET").upper()

        # No body for GET/DELETE requests
        if http_method in ["GET", "DELETE"]:
            return None

        # Extract path parameters from URL template
        url_path = operation.get("url_path", "")
        import re

        path_params = set(re.findall(r"\{([^}]+)\}", url_path))

        # Filter out path parameters, keep only body parameters
        body_params = {
            key: value for key, value in parameters.items() if key not in path_params
        }

        # Return None if no body parameters
        if not body_params:
            return None

        return body_params

    def _map_http_error(
        self,
        error: Exception,
        operation_id: str,
        correlation_id: str,
    ) -> ErrorResponse:
        """Map HTTP errors to structured error responses.

        Implements AC #6 error mapping requirements:
        - Map HTTP status codes to error codes
        - Provide actionable error messages
        - Include remediation steps

        Args:
            error: HTTP error exception
            operation_id: Operation that failed
            correlation_id: Request correlation ID

        Returns:
            Structured ErrorResponse with code, message, context
        """
        timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

        if isinstance(error, httpx.HTTPStatusError):
            status_code = error.response.status_code

            # Map status codes to error responses
            if status_code == 404:
                # Extract resource type from operation namespace
                namespace = (
                    operation_id.split(".")[0] if "." in operation_id else "resource"
                )
                return ErrorResponse(
                    error_code="RESOURCE_NOT_FOUND",
                    message=f"Resource not found: {namespace}",
                    context={
                        "operation_id": operation_id,
                        "http_status": status_code,
                        "remediation": "Verify the resource exists and parameters are correct",
                    },
                    correlation_id=correlation_id,
                    timestamp=timestamp,
                )

            elif status_code == 401:
                return ErrorResponse(
                    error_code="AUTHENTICATION_FAILED",
                    message="Authentication failed - check credentials",
                    context={
                        "operation_id": operation_id,
                        "http_status": status_code,
                        "remediation": "Verify username and password are correct",
                    },
                    correlation_id=correlation_id,
                    timestamp=timestamp,
                )

            elif status_code == 403:
                return ErrorResponse(
                    error_code="PERMISSION_DENIED",
                    message="Permission denied for operation",
                    context={
                        "operation_id": operation_id,
                        "http_status": status_code,
                        "remediation": "Verify user has permissions for this vhost and operation",
                    },
                    correlation_id=correlation_id,
                    timestamp=timestamp,
                )

            elif status_code >= 500:
                # Try to extract error details from response
                error_details = "Server error"
                try:
                    error_details = error.response.text
                except Exception:
                    pass

                return ErrorResponse(
                    error_code="RABBITMQ_SERVER_ERROR",
                    message=f"RabbitMQ server error: {error_details}",
                    context={
                        "operation_id": operation_id,
                        "http_status": status_code,
                        "remediation": "Check RabbitMQ server logs for details",
                    },
                    correlation_id=correlation_id,
                    timestamp=timestamp,
                )

        elif isinstance(error, httpx.TimeoutException):
            return ErrorResponse(
                error_code="OPERATION_TIMEOUT",
                message=f"Operation timed out after {self.http_client.timeout}s",
                context={
                    "operation_id": operation_id,
                    "remediation": "Try again or increase timeout setting",
                },
                correlation_id=correlation_id,
                timestamp=timestamp,
            )

        elif isinstance(error, httpx.ConnectError):
            return ErrorResponse(
                error_code="CONNECTION_FAILED",
                message=f"Failed to connect to RabbitMQ at {self.http_client.base_url}",
                context={
                    "operation_id": operation_id,
                    "remediation": "Verify RabbitMQ is running and Management API is enabled",
                },
                correlation_id=correlation_id,
                timestamp=timestamp,
            )

        # Generic error fallback
        return ErrorResponse(
            error_code="UNKNOWN_ERROR",
            message=str(error),
            context={
                "operation_id": operation_id,
                "error_type": type(error).__name__,
            },
            correlation_id=correlation_id,
            timestamp=timestamp,
        )

    async def execute(
        self,
        operation_id: str,
        parameters: dict[str, Any],
    ) -> ExecutionResult:
        """Execute RabbitMQ Management API operation.

        Implements AC #2, #3, #5 execution flow:
        1. Load operation metadata from registry
        2. Validate operation_id exists
        3. Validate parameters (basic validation, detailed in Task 6)
        4. Build HTTP request (URL, method, headers, body)
        5. Execute HTTP request via HTTPClient
        6. Parse and validate response
        7. Build structured result

        Args:
            operation_id: Operation ID from registry (e.g., "queues.list")
            parameters: Operation-specific parameters

        Returns:
            ExecutionResult with status, result, correlation_id, metadata

        Raises:
            KeyError: Operation not found in registry
            ValidationError: Parameter validation failed
            httpx.HTTPError: HTTP request failed
        """
        # Generate correlation ID for request tracing
        correlation_id = str(uuid.uuid4())
        start_time = time.perf_counter()

        # Sanitize parameters for logging (remove sensitive data)
        safe_params = {
            k: "***" if k in ["password", "token", "secret"] else v
            for k, v in parameters.items()
        }

        # Log operation execution start
        logger.info(
            "operation.execute_start",
            operation_id=operation_id,
            parameters=safe_params,
            correlation_id=correlation_id,
        )

        # Step 1: Load operation metadata
        operations = self.operation_registry.get("operations", {})
        if operation_id not in operations:
            logger.warning(
                "operation.not_found",
                operation_id=operation_id,
                correlation_id=correlation_id,
            )
            raise KeyError(
                f"Operation '{operation_id}' not found. "
                "Use search-ids to discover operations."
            )

        operation = operations[operation_id]

        # Step 2: Build HTTP request
        url_path = self._build_url(
            operation.get("url_path", ""),
            parameters,
        )

        http_method = operation.get("http_method", "GET")
        request_body = self._build_request_body(operation, parameters)

        # Sanitize URL for logging (remove sensitive data)
        safe_url = url_path.replace(self.http_client.password, "***")

        # Log HTTP request
        logger.info(
            "http.request_start",
            method=http_method,
            url=safe_url,
            body_size=len(str(request_body)) if request_body else 0,
            correlation_id=correlation_id,
        )

        # Step 3: Execute HTTP request
        try:
            response = await self.http_client.request(
                method=http_method,
                path=url_path,
                json=request_body,
            )

            # Raise for HTTP error status codes
            response.raise_for_status()

            # Step 4: Parse response
            end_time = time.perf_counter()
            duration_ms = (end_time - start_time) * 1000

            # Parse JSON response if available
            try:
                result_data = response.json() if response.text else {}
            except Exception:
                result_data = {"raw_response": response.text}

            # Log HTTP response
            logger.info(
                "http.response_success",
                status_code=response.status_code,
                response_time_ms=round(duration_ms, 2),
                body_size=len(response.text),
                correlation_id=correlation_id,
            )

            # Log operation completion
            logger.info(
                "operation.execute_success",
                operation_id=operation_id,
                duration_ms=round(duration_ms, 2),
                correlation_id=correlation_id,
            )

            # Step 5: Build execution result
            return ExecutionResult(
                status="success",
                result=result_data,
                correlation_id=correlation_id,
                metadata={
                    "operation_id": operation_id,
                    "http_status_code": response.status_code,
                    "response_time_ms": round(duration_ms, 2),
                },
            )

        except Exception as error:
            # Log error
            end_time = time.perf_counter()
            duration_ms = (end_time - start_time) * 1000

            logger.error(
                "operation.execute_error",
                operation_id=operation_id,
                error_type=type(error).__name__,
                error_message=str(error),
                duration_ms=round(duration_ms, 2),
                correlation_id=correlation_id,
            )

            # Map errors to structured responses
            error_response = self._map_http_error(error, operation_id, correlation_id)

            # Re-raise as ExecutionResult with error status
            return ExecutionResult(
                status="error",
                result=error_response.model_dump(),
                correlation_id=correlation_id,
                metadata={
                    "operation_id": operation_id,
                    "response_time_ms": round(duration_ms, 2),
                },
            )


__all__ = ["OperationExecutor", "ExecutionResult", "ErrorResponse"]
