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/>.
"""

"""Topology operations for queues using HTTP Management API (Story 3.1)."""

import re
import time
from typing import Any, NoReturn
from urllib.parse import quote

import httpx

from config.logging import get_logger
from rabbitmq_mcp_connection.http_client import HTTPClientManager
from rabbitmq_mcp_connection.middleware.vhost_validator import validate_vhost
from rabbitmq_mcp_connection.schemas.topology import (
    BindingInfo,
    CreateBindingRequest,
    CreateExchangeRequest,
    CreateQueueRequest,
    DeleteBindingRequest,
    DeleteBindingResponse,
    DeleteExchangeRequest,
    DeleteExchangeResponse,
    DeleteQueueRequest,
    ExchangeInfo,
    ListBindingsRequest,
    ListExchangesRequest,
    PurgeQueueRequest,
    QueueDetails,
)

logger = get_logger(__name__)


class ListQueuesOperation:
    """List queues operation with filtering, sorting, and pagination (AC #1-8).

    Implements async queue listing using HTTPClientManager to call
    RabbitMQ Management API GET /api/queues/{vhost} endpoint.
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize ListQueuesOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client

    @validate_vhost
    async def execute(
        self,
        vhost: str,
        name_pattern: str | None = None,
        state: str | None = None,
        consumers: int | None = None,
        consumer_operator: str = "=",
        sort_by: str | None = None,
        reverse: bool = False,
        page: int = 0,
        page_size: int = 100,
        correlation_id: str | None = None,
    ) -> list[QueueDetails]:
        """Execute list queues operation (AC #1-8).

        Args:
            vhost: Virtual host name (required, AC #1)
            name_pattern: Regex pattern to filter queue names (optional, AC #7)
            state: Filter by queue state: running, idle, flow (optional, AC #7)
            consumers: Filter by consumer count (optional, AC #7)
            consumer_operator: Operator for consumer filter: =, >, < (optional, AC #7, Task 3)
            sort_by: Sort field: name, messages, consumers, memory (optional, AC #8)
            reverse: Sort in descending order (optional, AC #8)
            page: Page number (0-indexed, optional, AC #4)
            page_size: Items per page (optional, AC #4)
            correlation_id: Correlation ID for audit trail (optional, ADR-009)

        Returns:
            List of QueueDetails models

        Raises:
            ValueError: If vhost does not exist (AC #6) or invalid operator
            httpx.TimeoutException: If request times out (AC #4)
        """
        # URL encode vhost (AC: Architecture Constraints)
        encoded_vhost = quote(vhost, safe="")
        endpoint = f"/queues/{encoded_vhost}"

        logger.info(
            "queues.list.start",
            message=f"Listing queues for vhost={vhost}",
            operation="queues.list",
            vhost=vhost,
            correlation_id=correlation_id,
        )

        start_time = time.time()

        try:
            # Call Management API (AC #1, #2, #3)
            response = await self.http_client.request("GET", endpoint)

            # Handle 404 - vhost not found (AC #6)
            if response.status_code == 404:
                error_msg = f"Virtual host '{vhost}' does not exist"
                logger.error(
                    "queues.list.vhost_not_found",
                    message=error_msg,
                    operation="queues.list",
                    vhost=vhost,
                    status_code=404,
                    correlation_id=correlation_id,
                )
                raise ValueError(error_msg)

            response.raise_for_status()

            # Parse response JSON (AC #1, #2, #3)
            queues_data = response.json()

            # Handle empty vhost (AC #5)
            if not queues_data:
                duration_ms = (time.time() - start_time) * 1000
                logger.info(
                    "queues.list.complete",
                    message=f"Listed 0 queues in {duration_ms:.2f}ms",
                    operation="queues.list",
                    vhost=vhost,
                    duration_ms=duration_ms,
                    queue_count=0,
                    correlation_id=correlation_id,
                )
                return []

            # Parse into QueueDetails models
            queues = [self._parse_queue(q) for q in queues_data]

            # Apply filters (AC #7)
            if name_pattern:
                queues = self.filter_by_name_pattern(queues, name_pattern)
                logger.debug(
                    "queues.list.filter",
                    message=f"Applied name_pattern filter: {name_pattern}",
                    operation="queues.list",
                    name_pattern=name_pattern,
                    remaining_count=len(queues),
                    correlation_id=correlation_id,
                )

            if state:
                queues = self.filter_by_state(queues, state)
                logger.debug(
                    "queues.list.filter",
                    message=f"Applied state filter: {state}",
                    operation="queues.list",
                    state=state,
                    remaining_count=len(queues),
                    correlation_id=correlation_id,
                )

            if consumers is not None:
                queues = self.filter_by_consumer_count(
                    queues, consumers, consumer_operator
                )
                logger.debug(
                    "queues.list.filter",
                    message=f"Applied consumers filter: {consumer_operator}{consumers}",
                    operation="queues.list",
                    consumers=consumers,
                    consumer_operator=consumer_operator,
                    remaining_count=len(queues),
                    correlation_id=correlation_id,
                )

            # Apply sorting (AC #8)
            if sort_by:
                queues = self.sort_by(queues, sort_by, reverse)
                logger.debug(
                    "queues.list.sort",
                    message=f"Sorted queues by {sort_by} ({'desc' if reverse else 'asc'})",
                    operation="queues.list",
                    sort_by=sort_by,
                    direction="desc" if reverse else "asc",
                    correlation_id=correlation_id,
                )

            # Apply pagination (AC #4)
            total_count = len(queues)
            start = page * page_size
            end = start + page_size
            queues = queues[start:end]

            duration_ms = (time.time() - start_time) * 1000
            logger.info(
                "queues.list.complete",
                message=f"Listed {len(queues)} queues in {duration_ms:.2f}ms",
                operation="queues.list",
                vhost=vhost,
                duration_ms=duration_ms,
                queue_count=len(queues),
                total_count=total_count,
                page=page,
                page_size=page_size,
                correlation_id=correlation_id,
            )

            return queues

        except httpx.TimeoutException as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "queues.list.timeout",
                message=f"Request timeout after {duration_ms:.2f}ms",
                operation="queues.list",
                vhost=vhost,
                duration_ms=duration_ms,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise
        except httpx.HTTPStatusError as e:
            logger.error(
                "queues.list.http_error",
                message=f"HTTP error: {e.response.status_code}",
                operation="queues.list",
                vhost=vhost,
                status_code=e.response.status_code,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise
        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "queues.list.error",
                message=f"Failed to list queues: {e}",
                operation="queues.list",
                vhost=vhost,
                duration_ms=duration_ms,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

    def _parse_queue(self, data: dict[str, Any]) -> QueueDetails:
        """Parse queue data from Management API response.

        Args:
            data: Queue data dictionary from API

        Returns:
            QueueDetails model
        """
        return QueueDetails(
            name=data.get("name", ""),
            vhost=data.get("vhost", ""),
            durable=data.get("durable", False),
            auto_delete=data.get("auto_delete", False),
            arguments=data.get("arguments", {}),
            messages=data.get("messages", 0),
            messages_ready=data.get("messages_ready", 0),
            messages_unacknowledged=data.get("messages_unacknowledged", 0),
            consumers=data.get("consumers", 0),
            memory=data.get("memory", 0),
            state=data.get("state", "idle"),
            message_rates=data.get("message_stats"),
        )

    def filter_by_name_pattern(
        self, queues: list[QueueDetails], pattern: str
    ) -> list[QueueDetails]:
        """Filter queues by name pattern (AC #7).

        Args:
            queues: List of QueueDetails
            pattern: Regex pattern to match queue names

        Returns:
            Filtered list of QueueDetails

        Raises:
            ValueError: If regex pattern is invalid
        """
        try:
            regex = re.compile(pattern)
        except re.error as e:
            raise ValueError(f"Invalid regex pattern: {e}")

        return [q for q in queues if regex.search(q.name)]

    def filter_by_state(
        self, queues: list[QueueDetails], state: str
    ) -> list[QueueDetails]:
        """Filter queues by state (AC #7).

        Args:
            queues: List of QueueDetails
            state: Queue state to filter by (running, idle, flow)

        Returns:
            Filtered list of QueueDetails
        """
        return [q for q in queues if q.state == state]

    def filter_by_consumer_count(
        self, queues: list[QueueDetails], count: int, operator: str = "="
    ) -> list[QueueDetails]:
        """Filter queues by consumer count with operators (AC #7, Task 3).

        Args:
            queues: List of QueueDetails
            count: Consumer count to filter by
            operator: Comparison operator: "=" (equals), ">" (greater than), "<" (less than)

        Returns:
            Filtered list of QueueDetails

        Raises:
            ValueError: If operator is not supported
        """
        if operator == "=":
            return [q for q in queues if q.consumers == count]
        elif operator == ">":
            return [q for q in queues if q.consumers > count]
        elif operator == "<":
            return [q for q in queues if q.consumers < count]
        else:
            raise ValueError(
                f"Unsupported operator '{operator}'. " f"Supported operators: =, >, <"
            )

    def sort_by(
        self, queues: list[QueueDetails], field: str, reverse: bool = False
    ) -> list[QueueDetails]:
        """Sort queues by field (AC #8).

        Args:
            queues: List of QueueDetails
            field: Field to sort by (name, messages, consumers, memory)
            reverse: Sort in descending order

        Returns:
            Sorted list of QueueDetails

        Raises:
            ValueError: If field is not supported
        """
        supported_fields = {"name", "messages", "consumers", "memory"}
        if field not in supported_fields:
            raise ValueError(
                f"Unsupported sort field '{field}'. "
                f"Supported fields: {', '.join(sorted(supported_fields))}"
            )

        # Handle None values: treat as 0 for numeric fields, empty string for name
        def sort_key(queue: QueueDetails) -> Any:
            value = getattr(queue, field)
            if value is None:
                return 0 if field != "name" else ""
            return value

        return sorted(queues, key=sort_key, reverse=reverse)


class CreateQueueOperation:
    """Create queue operation with idempotency and safety checks (Story 3.2, AC #1-8).

    Implements async queue creation using HTTPClientManager to call
    RabbitMQ Management API PUT /api/queues/{vhost}/{name} endpoint.
    Includes idempotent creation checks and conflict detection.
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize CreateQueueOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client

    @validate_vhost
    async def execute(
        self,
        request: CreateQueueRequest,
        correlation_id: str | None = None,
    ) -> dict[str, Any]:
        """Execute create queue operation with idempotency (AC #1-8).

        Args:
            request: CreateQueueRequest with queue configuration
            correlation_id: Correlation ID for audit trail (optional, ADR-009)

        Returns:
            Dict with status="success" and queue_details

        Raises:
            ValueError: If queue exists with different configuration (AC #5)
            httpx.TimeoutException: If request times out (AC #8)
        """
        start_time = time.time()

        # URL encode vhost and queue name (AC: Architecture Constraints)
        encoded_vhost = quote(request.vhost, safe="")
        encoded_name = quote(request.name, safe="")

        logger.info(
            "queue.create.start",
            message=f"Creating queue '{request.name}' in vhost '{request.vhost}'",
            operation="queue.create",
            name=request.name,
            vhost=request.vhost,
            durable=request.durable,
            auto_delete=request.auto_delete,
            arguments=request.arguments,
            correlation_id=correlation_id,
        )

        try:
            # Check if queue already exists (AC #4, #5, Task 3)
            existing_queue = await self._get_existing_queue(
                encoded_vhost, encoded_name, correlation_id
            )

            if existing_queue:
                # Queue exists - check if properties match (AC #4, #5)
                if self._properties_match(request, existing_queue):
                    # Idempotent creation - properties match (AC #4)
                    duration_ms = (time.time() - start_time) * 1000
                    logger.info(
                        "queue.create.idempotent",
                        message=f"Queue '{request.name}' already exists with matching properties",
                        operation="queue.create",
                        name=request.name,
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        correlation_id=correlation_id,
                    )

                    # Check for slow operation (AC #8)
                    if duration_ms > 100:
                        logger.warning(
                            "queue.create.slow",
                            message=f"Queue creation exceeded threshold: {duration_ms:.2f}ms",
                            operation="queue.create",
                            name=request.name,
                            vhost=request.vhost,
                            duration_ms=duration_ms,
                            threshold_ms=100,
                            correlation_id=correlation_id,
                        )

                    return {
                        "status": "success",
                        "queue_details": existing_queue.model_dump(),
                    }
                else:
                    # Properties don't match - conflict (AC #5)
                    expected = {
                        "durable": request.durable,
                        "auto_delete": request.auto_delete,
                        "arguments": request.arguments or {},
                    }
                    actual = {
                        "durable": existing_queue.durable,
                        "auto_delete": existing_queue.auto_delete,
                        "arguments": existing_queue.arguments,
                    }

                    error_msg = (
                        f"Queue '{request.name}' already exists with different configuration. "
                        f"Expected: {expected}, Actual: {actual}"
                    )

                    logger.error(
                        "queue.create.conflict",
                        message=error_msg,
                        operation="queue.create",
                        name=request.name,
                        vhost=request.vhost,
                        expected=expected,
                        actual=actual,
                        correlation_id=correlation_id,
                    )

                    raise ValueError(error_msg)

            # Queue doesn't exist - proceed with creation (AC #1, #2)
            endpoint = f"/queues/{encoded_vhost}/{encoded_name}"

            # Prepare request body (AC #1, #2)
            body: dict[str, Any] = {
                "durable": request.durable,
                "auto_delete": request.auto_delete,
            }

            if request.arguments:
                body["arguments"] = request.arguments

            # Create queue via PUT request (AC #1)
            response = await self.http_client.request("PUT", endpoint, json=body)

            # Handle 201 Created or 204 No Content (race condition - AC #1, Task 3)
            if response.status_code in [201, 204]:
                # Fetch created queue details
                queue_details = await self._get_existing_queue(
                    encoded_vhost, encoded_name, correlation_id
                )

                if not queue_details:
                    logger.error(
                        "queue.create.verification_failed",
                        message=f"Queue '{request.name}' created but could not retrieve details",
                        operation="queue.create",
                        name=request.name,
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )
                    raise ValueError(
                        f"Queue '{request.name}' created but verification failed"
                    )

                duration_ms = (time.time() - start_time) * 1000

                logger.info(
                    "queue.create.success",
                    message=f"Queue '{request.name}' created successfully in {duration_ms:.2f}ms",
                    operation="queue.create",
                    name=request.name,
                    vhost=request.vhost,
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                # Check for slow operation (AC #8)
                if duration_ms > 100:
                    logger.warning(
                        "queue.create.slow",
                        message=f"Queue creation exceeded threshold: {duration_ms:.2f}ms",
                        operation="queue.create",
                        name=request.name,
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        threshold_ms=100,
                        correlation_id=correlation_id,
                    )

                return {
                    "status": "success",
                    "queue_details": queue_details.model_dump(),
                }

            # Unexpected status code - raise HTTP error
            response.raise_for_status()

            # Should never reach here, but return error for safety
            raise ValueError(f"Unexpected response status: {response.status_code}")

        except ValueError:
            # Re-raise validation errors (AC #5, #6, #7)
            raise
        except httpx.TimeoutException as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "queue.create.timeout",
                message=f"Request timeout after {duration_ms:.2f}ms",
                operation="queue.create",
                name=request.name,
                vhost=request.vhost,
                duration_ms=duration_ms,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise
        except httpx.HTTPStatusError as e:
            logger.error(
                "queue.create.http_error",
                message=f"HTTP error: {e.response.status_code}",
                operation="queue.create",
                name=request.name,
                vhost=request.vhost,
                status_code=e.response.status_code,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise
        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "queue.create.error",
                message=f"Failed to create queue: {e}",
                operation="queue.create",
                name=request.name,
                vhost=request.vhost,
                duration_ms=duration_ms,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

    async def _get_existing_queue(
        self, encoded_vhost: str, encoded_name: str, correlation_id: str | None = None
    ) -> QueueDetails | None:
        """Get existing queue details if it exists (Task 3).

        Args:
            encoded_vhost: URL-encoded vhost name
            encoded_name: URL-encoded queue name
            correlation_id: Correlation ID for audit trail

        Returns:
            QueueDetails if queue exists, None otherwise
        """
        try:
            endpoint = f"/queues/{encoded_vhost}/{encoded_name}"
            response = await self.http_client.request("GET", endpoint)

            if response.status_code == 404:
                return None

            response.raise_for_status()
            queue_data = response.json()

            return QueueDetails(
                name=queue_data.get("name", ""),
                vhost=queue_data.get("vhost", ""),
                durable=queue_data.get("durable", False),
                auto_delete=queue_data.get("auto_delete", False),
                arguments=queue_data.get("arguments", {}),
                messages=queue_data.get("messages", 0),
                messages_ready=queue_data.get("messages_ready", 0),
                messages_unacknowledged=queue_data.get("messages_unacknowledged", 0),
                consumers=queue_data.get("consumers", 0),
                memory=queue_data.get("memory", 0),
                state=queue_data.get("state", "idle"),
                message_rates=queue_data.get("message_stats"),
            )
        except Exception:
            # If GET fails, assume queue doesn't exist
            return None

    def _properties_match(
        self, request: CreateQueueRequest, existing: QueueDetails
    ) -> bool:
        """Check if request properties match existing queue (AC #4, #5, Task 3).

        Args:
            request: CreateQueueRequest with desired properties
            existing: QueueDetails of existing queue

        Returns:
            True if properties match, False otherwise
        """
        # Compare durable and auto_delete
        if request.durable != existing.durable:
            return False
        if request.auto_delete != existing.auto_delete:
            return False

        # Compare arguments (normalize None to empty dict)
        request_args = request.arguments or {}
        existing_args = existing.arguments or {}

        # Compare arguments (order-independent)
        return request_args == existing_args


class DeleteQueueOperation:
    """Delete queue operation with safety validation (Story 3.3, AC #1-7).

    Implements async queue deletion using HTTPClientManager to call
    RabbitMQ Management API DELETE /api/queues/{vhost}/{name} endpoint.
    Includes safety checks to prevent accidental deletion of queues
    with messages or consumers unless explicitly forced.
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize DeleteQueueOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client

    @validate_vhost
    async def execute(
        self,
        request: DeleteQueueRequest,
        correlation_id: str | None = None,
    ) -> dict[str, Any]:
        """Execute delete queue operation with safety checks (AC #1-7).

        Args:
            request: DeleteQueueRequest with queue name, vhost, and force flag
            correlation_id: Correlation ID for audit trail (optional, ADR-009)

        Returns:
            Dict with status="success" and message

        Raises:
            ValueError: If queue doesn't exist or safety check fails (AC #1, #5)
            httpx.TimeoutException: If request times out (AC #6)
        """
        start_time = time.time()

        # URL encode vhost and queue name
        encoded_vhost = quote(request.vhost, safe="")
        encoded_name = quote(request.name, safe="")

        logger.info(
            "queue.delete.start",
            message=f"Deleting queue '{request.name}' in vhost '{request.vhost}'",
            operation="queue.delete",
            name=request.name,
            vhost=request.vhost,
            force=request.force,
            correlation_id=correlation_id,
        )

        try:
            # Perform safety check unless force=True (AC #1, #2, #3, Task 2)
            if not request.force:
                await self._check_queue_safety(
                    encoded_vhost, encoded_name, request, correlation_id
                )

            # Perform DELETE request (AC #2, #4, Task 3)
            endpoint = f"/api/queues/{encoded_vhost}/{encoded_name}"

            response = await self.http_client.request(
                method="DELETE",
                path=endpoint,
            )

            # 204 No Content indicates successful deletion (AC #4)
            if response.status_code == 204:
                duration_ms = (time.time() - start_time) * 1000

                logger.info(
                    "queue.delete.success",
                    message=f"Queue '{request.name}' deleted successfully",
                    operation="queue.delete",
                    name=request.name,
                    vhost=request.vhost,
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                # Log if deletion was forced (AC #7, Task 5)
                if request.force:
                    logger.info(
                        "queue.delete.forced",
                        message=f"Queue '{request.name}' was force-deleted",
                        operation="queue.delete",
                        name=request.name,
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )

                # Check for slow operation (AC #6, Task 6)
                if duration_ms > 100:
                    logger.warning(
                        "queue.delete.slow",
                        message=f"Queue deletion exceeded threshold: {duration_ms:.2f}ms",
                        operation="queue.delete",
                        name=request.name,
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        threshold_ms=100,
                        correlation_id=correlation_id,
                    )

                return {
                    "status": "success",
                    "message": f"Queue '{request.name}' deleted",
                }

            # 404 after successful safety check = race condition (AC #5, Task 4)
            # Queue was deleted between GET and DELETE - treat as success
            if response.status_code == 404:
                duration_ms = (time.time() - start_time) * 1000

                logger.info(
                    "queue.delete.race_condition",
                    message=f"Queue '{request.name}' was already deleted (race condition)",
                    operation="queue.delete",
                    name=request.name,
                    vhost=request.vhost,
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                return {
                    "status": "success",
                    "message": f"Queue '{request.name}' deleted",
                }

            # Unexpected response
            logger.error(
                "queue.delete.unexpected_response",
                message=f"Unexpected response status: {response.status_code}",
                operation="queue.delete",
                name=request.name,
                vhost=request.vhost,
                status_code=response.status_code,
                correlation_id=correlation_id,
            )
            raise ValueError(
                f"Unexpected response from RabbitMQ API: {response.status_code}"
            )

        except httpx.TimeoutException:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "queue.delete.timeout",
                message=f"Queue deletion timed out after {duration_ms:.2f}ms",
                operation="queue.delete",
                name=request.name,
                vhost=request.vhost,
                duration_ms=duration_ms,
                correlation_id=correlation_id,
            )
            raise

        except ValueError as e:
            # Safety check or validation error
            logger.error(
                "queue.delete.validation_error",
                message=str(e),
                operation="queue.delete",
                name=request.name,
                vhost=request.vhost,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

        except Exception as e:
            logger.error(
                "queue.delete.error",
                message=f"Failed to delete queue: {str(e)}",
                operation="queue.delete",
                name=request.name,
                vhost=request.vhost,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

    async def _check_queue_safety(
        self,
        encoded_vhost: str,
        encoded_name: str,
        request: DeleteQueueRequest,
        correlation_id: str | None,
    ) -> None:
        """Check queue safety before deletion (AC #1, #3, Task 2).

        Retrieves queue statistics and blocks deletion if queue has
        messages or consumers (unless force=True).

        Args:
            encoded_vhost: URL-encoded vhost name
            encoded_name: URL-encoded queue name
            request: Original DeleteQueueRequest
            correlation_id: Correlation ID for audit trail

        Raises:
            ValueError: If queue doesn't exist or has messages/consumers (AC #1, #5)
        """
        endpoint = f"/api/queues/{encoded_vhost}/{encoded_name}"

        try:
            response = await self.http_client.request(
                method="GET",
                path=endpoint,
            )

            if response.status_code == 404:
                # Queue doesn't exist (AC #5, Task 4)
                raise ValueError(
                    f"Queue '{request.name}' not found in vhost '{request.vhost}'"
                )

            if response.status_code == 200:
                queue_data = response.json()
                messages = queue_data.get("messages", 0)
                consumers = queue_data.get("consumers", 0)

                # Log safety check decision (AC #7, Task 5)
                if messages > 0 or consumers > 0:
                    # Queue has messages or consumers - block deletion (AC #1)
                    logger.info(
                        "queue.delete.safety_check",
                        message="Safety check blocked deletion",
                        operation="queue.delete",
                        name=request.name,
                        vhost=request.vhost,
                        messages=messages,
                        consumers=consumers,
                        decision="blocked",
                        correlation_id=correlation_id,
                    )

                    raise ValueError(
                        f"Queue '{request.name}' has {messages} messages and {consumers} consumers. "
                        f"Use --force flag (CLI) or force=true parameter (API) to confirm deletion."
                    )
                else:
                    # Empty queue - allow deletion (AC #3)
                    logger.info(
                        "queue.delete.safety_check",
                        message="Safety check passed (empty queue)",
                        operation="queue.delete",
                        name=request.name,
                        vhost=request.vhost,
                        messages=0,
                        consumers=0,
                        decision="allowed",
                        correlation_id=correlation_id,
                    )

            else:
                # Unexpected response
                raise ValueError(
                    f"Failed to retrieve queue info: {response.status_code}"
                )

        except httpx.TimeoutException:
            logger.error(
                "queue.delete.safety_check_timeout",
                message=f"Safety check timed out for queue '{request.name}'",
                operation="queue.delete",
                name=request.name,
                vhost=request.vhost,
                correlation_id=correlation_id,
            )
            raise


class PurgeQueueOperation:
    """Purge queue operation removing all messages (Story 3.4, AC #1-8).

    Implements async queue purge using HTTPClientManager to call
    RabbitMQ Management API DELETE /api/queues/{vhost}/{name}/contents endpoint.
    Preserves queue configuration, settings, and consumer connections while
    removing all messages from the queue.
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize PurgeQueueOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client

    @validate_vhost
    async def execute(
        self,
        request: PurgeQueueRequest,
        correlation_id: str | None = None,
    ) -> dict[str, Any]:
        """Execute purge queue operation (AC #1-8).

        Args:
            request: PurgeQueueRequest with queue name and vhost
            correlation_id: Correlation ID for audit trail (optional, ADR-009)

        Returns:
            Dict with status="success", purged_message_count, queue_name, vhost

        Raises:
            ValueError: If queue doesn't exist (AC #7)
            httpx.TimeoutException: If request times out (AC #5)
        """
        start_time = time.time()

        # URL encode vhost and queue name
        encoded_vhost = quote(request.vhost, safe="")
        encoded_name = quote(request.name, safe="")

        logger.info(
            "queue.purge.start",
            message=f"Purging queue '{request.name}' in vhost '{request.vhost}'",
            operation="queue.purge",
            name=request.name,
            vhost=request.vhost,
            correlation_id=correlation_id,
        )

        try:
            # Perform DELETE /contents request (AC #1, #2, #3, Task 2)
            endpoint = f"/api/queues/{encoded_vhost}/{encoded_name}/contents"

            response = await self.http_client.request(
                method="DELETE",
                path=endpoint,
            )

            # 200 OK with body indicates successful purge (AC #4)
            if response.status_code == 200:
                duration_ms = (time.time() - start_time) * 1000

                # Parse response body to extract purged message count (AC #4, Task 2)
                response_data = response.json()
                purged_count = response_data.get("message_count", 0)

                # Log empty queue purge separately (AC #6, Task 5)
                if purged_count == 0:
                    logger.info(
                        "queue.purge.empty",
                        message=f"Queue '{request.name}' was already empty",
                        operation="queue.purge",
                        name=request.name,
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        correlation_id=correlation_id,
                    )
                else:
                    logger.info(
                        "queue.purge.success",
                        message=f"Queue '{request.name}' purged successfully",
                        operation="queue.purge",
                        name=request.name,
                        vhost=request.vhost,
                        purged_count=purged_count,
                        duration_ms=duration_ms,
                        correlation_id=correlation_id,
                    )

                # Log performance warning if exceeds threshold (AC #5, Task 6)
                if duration_ms > 500:
                    logger.warning(
                        "queue.purge.slow",
                        message="Purge operation exceeded 500ms threshold",
                        operation="queue.purge",
                        name=request.name,
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        threshold_ms=500,
                        purged_count=purged_count,
                        correlation_id=correlation_id,
                    )

                return {
                    "status": "success",
                    "purged_message_count": purged_count,
                    "queue_name": request.name,
                    "vhost": request.vhost,
                }

            # Handle 404 Not Found (AC #7, Task 4)
            elif response.status_code == 404:
                error_msg = (
                    f"Queue '{request.name}' not found in vhost " f"'{request.vhost}'"
                )

                logger.error(
                    "queue.purge.not_found",
                    message=error_msg,
                    operation="queue.purge",
                    name=request.name,
                    vhost=request.vhost,
                    correlation_id=correlation_id,
                )

                raise ValueError(error_msg)

            else:
                # Unexpected response
                error_msg = f"Failed to purge queue: HTTP {response.status_code}"
                logger.error(
                    "queue.purge.failed",
                    message=error_msg,
                    operation="queue.purge",
                    name=request.name,
                    vhost=request.vhost,
                    status_code=response.status_code,
                    correlation_id=correlation_id,
                )
                raise ValueError(error_msg)

        except httpx.TimeoutException:
            logger.error(
                "queue.purge.timeout",
                message=f"Purge operation timed out for queue '{request.name}'",
                operation="queue.purge",
                name=request.name,
                vhost=request.vhost,
                correlation_id=correlation_id,
            )
            raise

        except ValueError:
            # Re-raise validation errors (AC #7)
            raise

        except Exception as e:
            logger.error(
                "queue.purge.error",
                message=f"Unexpected error during purge: {str(e)}",
                operation="queue.purge",
                name=request.name,
                vhost=request.vhost,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise


class ListExchangesOperation:
    """List exchanges operation with filtering and sorting (Story 3.5, AC #1-8).

    Implements async exchange listing using HTTPClientManager to call
    RabbitMQ Management API GET /api/exchanges/{vhost} endpoint.
    Includes client-side filtering by name pattern and type, plus sorting.
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize ListExchangesOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client

    @validate_vhost
    async def execute(
        self,
        request: ListExchangesRequest,
        correlation_id: str | None = None,
    ) -> list[ExchangeInfo]:
        """Execute list exchanges operation (AC #1-8).

        Args:
            request: ListExchangesRequest with vhost and optional filters
            correlation_id: Correlation ID for audit trail (optional, ADR-009)

        Returns:
            List of ExchangeInfo models sorted and filtered per request

        Raises:
            ValueError: If vhost doesn't exist (AC #6)
            httpx.TimeoutException: If request times out (AC #4)
        """
        start_time = time.time()

        # URL encode vhost (AC: Architecture Constraints)
        encoded_vhost = quote(request.vhost, safe="")
        endpoint = f"/api/exchanges/{encoded_vhost}"

        logger.info(
            "exchange.list.start",
            message=f"Listing exchanges for vhost={request.vhost}",
            operation="exchange.list",
            vhost=request.vhost,
            filters={
                "name_pattern": request.name_pattern,
                "type": request.type,
            },
            sort_by=request.sort_by,
            correlation_id=correlation_id,
        )

        try:
            # Perform GET request (AC #1, Task 3)
            response = await self.http_client.request(
                method="GET",
                path=endpoint,
            )

            # 200 OK with JSON array (AC #1, #2, #3)
            if response.status_code == 200:
                duration_ms = (time.time() - start_time) * 1000

                # Parse response array into List[ExchangeInfo]
                exchanges_data = response.json()

                # Handle empty list (AC #5)
                if not exchanges_data:
                    logger.info(
                        "exchange.list.empty",
                        message=f"No exchanges found in vhost '{request.vhost}'",
                        operation="exchange.list",
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )
                    return []

                # Validate each exchange using Pydantic (AC #2, #3, Task 2)
                exchanges = [ExchangeInfo(**ex) for ex in exchanges_data]

                # Apply client-side filters (AC #7, Task 4)
                filtered_exchanges = self._apply_filters(
                    exchanges,
                    request.name_pattern,
                    request.type,
                )

                # Apply client-side sorting (AC #8, Task 5)
                sorted_exchanges = self._apply_sorting(
                    filtered_exchanges,
                    request.sort_by,
                )

                # Log successful list (Task 7)
                logger.info(
                    "exchange.list.success",
                    message=f"Listed {len(sorted_exchanges)} exchanges in vhost '{request.vhost}'",
                    operation="exchange.list",
                    vhost=request.vhost,
                    count=len(sorted_exchanges),
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                # Performance monitoring (AC #4, Task 8)
                if duration_ms > 200:
                    logger.warning(
                        "exchange.list.slow",
                        message="Exchange list operation exceeded 200ms threshold",
                        operation="exchange.list",
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        threshold_ms=200,
                        count=len(sorted_exchanges),
                        correlation_id=correlation_id,
                    )

                return sorted_exchanges

            # 404 Not Found - vhost doesn't exist (AC #6)
            elif response.status_code == 404:
                logger.warning(
                    "exchange.list.vhost_not_found",
                    message=f"Virtual host '{request.vhost}' does not exist",
                    operation="exchange.list",
                    vhost=request.vhost,
                    status_code=response.status_code,
                    correlation_id=correlation_id,
                )
                raise ValueError(f"Virtual host '{request.vhost}' does not exist")

            # Unexpected status code
            else:
                logger.error(
                    "exchange.list.unexpected_status",
                    message=f"Unexpected status code: {response.status_code}",
                    operation="exchange.list",
                    vhost=request.vhost,
                    status_code=response.status_code,
                    correlation_id=correlation_id,
                )
                raise ValueError(
                    f"Failed to list exchanges: HTTP {response.status_code}"
                )

        except httpx.TimeoutException:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "exchange.list.timeout",
                message=f"List exchanges request timed out after {duration_ms:.0f}ms",
                operation="exchange.list",
                vhost=request.vhost,
                duration_ms=duration_ms,
                correlation_id=correlation_id,
            )
            raise

        except ValueError:
            # Re-raise validation errors (vhost not found, etc.)
            raise

        except Exception as e:
            logger.error(
                "exchange.list.error",
                message=f"Unexpected error during list: {str(e)}",
                operation="exchange.list",
                vhost=request.vhost,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

    def _apply_filters(
        self,
        exchanges: list[ExchangeInfo],
        name_pattern: str | None,
        exchange_type: str | None,
    ) -> list[ExchangeInfo]:
        """Apply client-side filters to exchange list (AC #7, Task 4).

        Args:
            exchanges: List of ExchangeInfo to filter
            name_pattern: Regex pattern for name filtering (optional)
            exchange_type: Exchange type to filter by (optional)

        Returns:
            Filtered list of ExchangeInfo
        """
        filtered = exchanges

        # Filter by name pattern (regex)
        if name_pattern:
            try:
                pattern = re.compile(name_pattern)
                filtered = [ex for ex in filtered if pattern.match(ex.name)]
            except re.error as e:
                raise ValueError(f"Invalid regex pattern '{name_pattern}': {str(e)}")

        # Filter by type (exact match)
        if exchange_type:
            filtered = [ex for ex in filtered if ex.type == exchange_type]

        return filtered

    def _apply_sorting(
        self,
        exchanges: list[ExchangeInfo],
        sort_by: str,
    ) -> list[ExchangeInfo]:
        """Apply client-side sorting to exchange list (AC #8, Task 5).

        Args:
            exchanges: List of ExchangeInfo to sort
            sort_by: Sort field (name, type, message_in_rate)

        Returns:
            Sorted list of ExchangeInfo
        """
        if sort_by == "name":
            # Alphabetical sort (case-insensitive)
            return sorted(exchanges, key=lambda ex: ex.name.lower())

        elif sort_by == "type":
            # Alphabetical sort by type
            return sorted(exchanges, key=lambda ex: ex.type)

        elif sort_by == "message_in_rate":
            # Descending sort by message_in rate (handle None as 0)
            def get_rate(ex: ExchangeInfo) -> float:
                if ex.message_stats and ex.message_stats.publish_in is not None:
                    return ex.message_stats.publish_in
                return 0.0

            return sorted(exchanges, key=get_rate, reverse=True)

        else:
            # Default to name sort
            return sorted(exchanges, key=lambda ex: ex.name.lower())


class CreateExchangeOperation:
    """Create exchange operation with idempotency and conflict detection (Story 3.6, AC #1-9).

    Implements async exchange creation using HTTPClientManager to call
    RabbitMQ Management API PUT /api/exchanges/{vhost}/{name} endpoint.
    Includes idempotent creation (204 No Content) and conflict detection (409 Conflict).
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize CreateExchangeOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client

    @validate_vhost
    async def execute(
        self,
        request: CreateExchangeRequest,
        correlation_id: str | None = None,
    ) -> ExchangeInfo:
        """Execute create exchange operation with idempotency and conflict detection (AC #1-9).

        Args:
            request: CreateExchangeRequest with exchange configuration
            correlation_id: Correlation ID for audit trail (optional, ADR-009)

        Returns:
            ExchangeInfo model with created exchange details

        Raises:
            ValueError: If exchange exists with different properties (AC #6) or vhost not found
            httpx.TimeoutException: If request times out (AC #9)
        """
        start_time = time.time()

        # URL encode vhost and exchange name (Architecture Constraints)
        encoded_vhost = quote(request.vhost, safe="")
        encoded_name = quote(request.name, safe="")

        logger.info(
            "exchange.create.start",
            message=f"Creating exchange '{request.name}' in vhost '{request.vhost}'",
            operation="exchange.create",
            name=request.name,
            vhost=request.vhost,
            type=request.type,
            durable=request.durable,
            auto_delete=request.auto_delete,
            internal=request.internal,
            arguments=request.arguments,
            correlation_id=correlation_id,
        )

        try:
            # Construct endpoint (AC #1)
            endpoint = f"/api/exchanges/{encoded_vhost}/{encoded_name}"

            # Prepare request body (AC #1, #2, #3)
            body: dict[str, Any] = {
                "type": request.type,
                "durable": request.durable,
                "auto_delete": request.auto_delete,
                "internal": request.internal,
            }

            if request.arguments:
                body["arguments"] = request.arguments

            # Create exchange via PUT request (AC #1)
            response = await self.http_client.request("PUT", endpoint, json=body)

            # Handle 201 Created (success - new exchange created)
            if response.status_code == 201:
                # Fetch created exchange details
                exchange_info = await self._get_exchange_details(
                    encoded_vhost, encoded_name, correlation_id
                )

                if not exchange_info:
                    logger.error(
                        "exchange.create.verification_failed",
                        message=f"Exchange '{request.name}' created but could not retrieve details",
                        operation="exchange.create",
                        name=request.name,
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )
                    raise ValueError(
                        f"Exchange '{request.name}' created but verification failed"
                    )

                duration_ms = (time.time() - start_time) * 1000

                logger.info(
                    "exchange.create.success",
                    message=f"Exchange '{request.name}' created successfully in {duration_ms:.2f}ms",
                    operation="exchange.create",
                    name=request.name,
                    vhost=request.vhost,
                    type=request.type,
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                # Check for slow operation (AC #9)
                if duration_ms > 100:
                    logger.warning(
                        "exchange.create.slow",
                        message=f"Exchange creation exceeded threshold: {duration_ms:.2f}ms",
                        operation="exchange.create",
                        name=request.name,
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        threshold_ms=100,
                        correlation_id=correlation_id,
                    )

                return exchange_info

            # Handle 204 No Content (idempotent - exchange already exists with same properties, AC #5)
            elif response.status_code == 204:
                # Fetch existing exchange details
                exchange_info = await self._get_exchange_details(
                    encoded_vhost, encoded_name, correlation_id
                )

                if not exchange_info:
                    logger.error(
                        "exchange.create.verification_failed",
                        message=f"Exchange '{request.name}' exists but could not retrieve details",
                        operation="exchange.create",
                        name=request.name,
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )
                    raise ValueError(
                        f"Exchange '{request.name}' exists but verification failed"
                    )

                duration_ms = (time.time() - start_time) * 1000

                logger.info(
                    "exchange.create.idempotent",
                    message=f"Exchange '{request.name}' already exists with same properties",
                    operation="exchange.create",
                    name=request.name,
                    vhost=request.vhost,
                    type=request.type,
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                # Check for slow operation (AC #9)
                if duration_ms > 100:
                    logger.warning(
                        "exchange.create.slow",
                        message=f"Exchange creation exceeded threshold: {duration_ms:.2f}ms",
                        operation="exchange.create",
                        name=request.name,
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        threshold_ms=100,
                        correlation_id=correlation_id,
                    )

                return exchange_info

            # Handle 404 Not Found (vhost doesn't exist)
            elif response.status_code == 404:
                error_msg = f"Virtual host '{request.vhost}' does not exist"
                logger.error(
                    "exchange.create.vhost_not_found",
                    message=error_msg,
                    operation="exchange.create",
                    name=request.name,
                    vhost=request.vhost,
                    correlation_id=correlation_id,
                )
                raise ValueError(error_msg)

            # Handle 409 Conflict (exchange exists with different properties, AC #6)
            elif response.status_code == 409:
                # Parse error response
                error_msg = self._handle_conflict_error(
                    request, response, correlation_id
                )
                raise ValueError(error_msg)

            # Unexpected status code - raise HTTP error
            response.raise_for_status()

            # Should never reach here, but return error for safety
            raise ValueError(f"Unexpected response status: {response.status_code}")

        except ValueError:
            # Re-raise validation errors (AC #5, #6, #7, #8)
            raise
        except httpx.TimeoutException as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "exchange.create.timeout",
                message=f"Request timeout after {duration_ms:.2f}ms",
                operation="exchange.create",
                name=request.name,
                vhost=request.vhost,
                duration_ms=duration_ms,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise
        except httpx.HTTPStatusError as e:
            logger.error(
                "exchange.create.http_error",
                message=f"HTTP error: {e.response.status_code}",
                operation="exchange.create",
                name=request.name,
                vhost=request.vhost,
                status_code=e.response.status_code,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise
        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "exchange.create.error",
                message=f"Failed to create exchange: {e}",
                operation="exchange.create",
                name=request.name,
                vhost=request.vhost,
                duration_ms=duration_ms,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

    async def _get_exchange_details(
        self,
        encoded_vhost: str,
        encoded_name: str,
        correlation_id: str | None,
    ) -> ExchangeInfo | None:
        """Fetch exchange details from RabbitMQ Management API.

        Args:
            encoded_vhost: URL-encoded vhost name
            encoded_name: URL-encoded exchange name
            correlation_id: Correlation ID for audit trail

        Returns:
            ExchangeInfo model or None if exchange not found
        """
        try:
            endpoint = f"/api/exchanges/{encoded_vhost}/{encoded_name}"
            response = await self.http_client.request("GET", endpoint)

            if response.status_code == 200:
                data = response.json()
                return ExchangeInfo(**data)

            return None

        except Exception as e:
            logger.warning(
                "exchange.get_details.error",
                message=f"Failed to fetch exchange details: {e}",
                operation="exchange.create",
                vhost_encoded=encoded_vhost,
                name_encoded=encoded_name,
                error=str(e),
                correlation_id=correlation_id,
            )
            return None

    def _handle_conflict_error(
        self,
        request: CreateExchangeRequest,
        response: httpx.Response,
        correlation_id: str | None,
    ) -> str:
        """Handle 409 Conflict error and format user-friendly message (AC #6, Task 4).

        Args:
            request: Original CreateExchangeRequest
            response: HTTP response with 409 status
            correlation_id: Correlation ID for audit trail

        Returns:
            User-friendly error message
        """
        # Try to parse RabbitMQ error response
        try:
            error_data = response.json()
            reason = error_data.get("reason", "Unknown conflict reason")
        except Exception:
            reason = "Exchange exists with different properties"

        # Format user-friendly error message
        error_msg = (
            f"Exchange '{request.name}' already exists with different properties. "
            f"Conflict: {reason}"
        )

        # Try to extract type mismatch if present
        if "type" in reason.lower():
            error_msg = (
                f"Exchange '{request.name}' already exists with a different type. "
                f"Cannot change exchange type. Delete and recreate if needed."
            )

        logger.error(
            "exchange.create.conflict",
            message=error_msg,
            operation="exchange.create",
            name=request.name,
            vhost=request.vhost,
            requested_type=request.type,
            reason=reason,
            correlation_id=correlation_id,
        )

        return error_msg


class DeleteExchangeOperation:
    """Delete exchange operation with protection and safety validation (Story 3.7, AC #1-8).

    Implements async exchange deletion using HTTPClientManager to call
    RabbitMQ Management API DELETE /api/exchanges/{vhost}/{name} endpoint.
    Includes safety checks to prevent deletion of exchanges with active bindings
    unless explicitly forced, and protection for system exchanges and default exchange.
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize DeleteExchangeOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client

    @validate_vhost
    async def execute(
        self,
        request: DeleteExchangeRequest,
        correlation_id: str | None = None,
    ) -> DeleteExchangeResponse:
        """Execute delete exchange operation with safety checks (AC #1-8).

        Args:
            request: DeleteExchangeRequest with exchange name, vhost, and force flag
            correlation_id: Correlation ID for audit trail (optional, ADR-009)

        Returns:
            DeleteExchangeResponse with status and message

        Raises:
            ValueError: If safety check fails (AC #1) or protected exchange (AC #4, #5)
            httpx.TimeoutException: If request times out (AC #7)
        """
        from rabbitmq_mcp_connection.schemas.topology import DeleteExchangeResponse

        start_time = time.time()

        # URL encode vhost and exchange name
        encoded_vhost = quote(request.vhost, safe="")
        encoded_name = quote(request.name, safe="")

        logger.info(
            "exchange.delete.start",
            message=f"Deleting exchange '{request.name}' in vhost '{request.vhost}'",
            operation="exchange.delete",
            name=request.name,
            vhost=request.vhost,
            force=request.force,
            correlation_id=correlation_id,
        )

        try:
            # Perform safety check unless force=True (AC #1, #2, #3, Task 3)
            if not request.force:
                await self._check_exchange_bindings(
                    encoded_vhost, encoded_name, request, correlation_id
                )

            # Perform DELETE request (AC #2, #3, #6, Task 4)
            endpoint = f"/api/exchanges/{encoded_vhost}/{encoded_name}"

            # Add if-unused query parameter for server-side validation when not forced
            if not request.force:
                endpoint += "?if-unused=true"

            response = await self.http_client.request(
                method="DELETE",
                path=endpoint,
            )

            # 204 No Content indicates successful deletion (AC #6)
            if response.status_code == 204:
                duration_ms = (time.time() - start_time) * 1000

                logger.info(
                    "exchange.delete.success",
                    message=f"Exchange '{request.name}' deleted successfully",
                    operation="exchange.delete",
                    name=request.name,
                    vhost=request.vhost,
                    forced=request.force,
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                # Check for slow operation (AC #7, Task 7)
                if duration_ms > 100:
                    logger.warning(
                        "exchange.delete.slow",
                        message=f"Exchange deletion exceeded threshold: {duration_ms:.2f}ms",
                        operation="exchange.delete",
                        name=request.name,
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        threshold_ms=100,
                        correlation_id=correlation_id,
                    )

                return DeleteExchangeResponse(
                    status="success",
                    message=f"Exchange '{request.name}' deleted",
                    vhost=request.vhost,
                    name=request.name,
                )

            # 404 indicates exchange doesn't exist - treat as idempotent success
            if response.status_code == 404:
                duration_ms = (time.time() - start_time) * 1000

                logger.info(
                    "exchange.delete.not_found",
                    message=f"Exchange '{request.name}' not found (already deleted or never existed)",
                    operation="exchange.delete",
                    name=request.name,
                    vhost=request.vhost,
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                return DeleteExchangeResponse(
                    status="success",
                    message=f"Exchange '{request.name}' deleted",
                    vhost=request.vhost,
                    name=request.name,
                )

            # Other error responses
            response.raise_for_status()

            # Fallback (should not reach here)
            raise ValueError(f"Unexpected response status: {response.status_code}")

        except ValueError:
            # Re-raise ValueError from safety checks or protected exchanges
            raise

        except httpx.TimeoutException as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "exchange.delete.timeout",
                message=f"Exchange deletion timed out after {duration_ms:.2f}ms",
                operation="exchange.delete",
                name=request.name,
                vhost=request.vhost,
                duration_ms=duration_ms,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "exchange.delete.error",
                message=f"Failed to delete exchange '{request.name}'",
                operation="exchange.delete",
                name=request.name,
                vhost=request.vhost,
                duration_ms=duration_ms,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

    async def _check_exchange_bindings(
        self,
        encoded_vhost: str,
        encoded_name: str,
        request: DeleteExchangeRequest,
        correlation_id: str | None,
    ) -> None:
        """Check exchange bindings and block deletion if bindings exist (AC #1, #2, #3, Task 3).

        Args:
            encoded_vhost: URL-encoded vhost name
            encoded_name: URL-encoded exchange name
            request: DeleteExchangeRequest with exchange details
            correlation_id: Correlation ID for audit trail

        Raises:
            ValueError: If exchange has active bindings and force=False
        """
        try:
            # Get bindings where this exchange is the source (AC #1, Task 3)
            endpoint = f"/api/exchanges/{encoded_vhost}/{encoded_name}/bindings/source"
            response = await self.http_client.request("GET", endpoint)

            # 404 means exchange doesn't exist - let DELETE handle it
            if response.status_code == 404:
                return

            response.raise_for_status()
            bindings = response.json()
            bindings_count = len(bindings)

            # If exchange has bindings, block deletion (AC #1, #2)
            if bindings_count > 0:
                error_msg = (
                    f"Exchange '{request.name}' has {bindings_count} active bindings. "
                    f"Use force=true to confirm deletion."
                )

                logger.warning(
                    "exchange.delete.blocked",
                    message=error_msg,
                    operation="exchange.delete",
                    name=request.name,
                    vhost=request.vhost,
                    bindings_count=bindings_count,
                    correlation_id=correlation_id,
                )

                raise ValueError(error_msg)

            # No bindings - safe to delete (AC #3)
            logger.debug(
                "exchange.delete.safe",
                message=f"Exchange '{request.name}' has no bindings, safe to delete",
                operation="exchange.delete",
                name=request.name,
                vhost=request.vhost,
                correlation_id=correlation_id,
            )

        except ValueError:
            # Re-raise ValueError from binding check
            raise

        except Exception as e:
            # Log warning but allow deletion to proceed
            # RabbitMQ API will be the final authority
            logger.warning(
                "exchange.delete.binding_check_failed",
                message=f"Failed to check exchange bindings: {e}",
                operation="exchange.delete",
                name=request.name,
                vhost=request.vhost,
                error=str(e),
                correlation_id=correlation_id,
            )


class ListBindingsOperation:
    """List bindings operation with filtering and sorting (Story 3.8, AC #1-7).

    Implements async binding listing using HTTPClientManager to call
    RabbitMQ Management API GET /api/bindings/{vhost} endpoint.
    Supports both exchange-to-queue and exchange-to-exchange bindings.
    Includes client-side filtering by source, destination, destination_type
    and sorting by source, destination, or routing_key.
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize ListBindingsOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client

    @validate_vhost
    async def execute(
        self,
        request: ListBindingsRequest,
        correlation_id: str | None = None,
    ) -> list[BindingInfo]:
        """Execute list bindings operation (AC #1-7).

        Args:
            request: ListBindingsRequest with vhost and optional filters
            correlation_id: Correlation ID for audit trail (optional, ADR-009)

        Returns:
            List of BindingInfo models sorted and filtered per request

        Raises:
            ValueError: If vhost doesn't exist (caught by VhostValidator, AC #1)
            httpx.TimeoutException: If request times out (AC #5)
        """
        start_time = time.time()

        # URL encode vhost (Architecture Constraints)
        encoded_vhost = quote(request.vhost, safe="")
        endpoint = f"/api/bindings/{encoded_vhost}"

        logger.info(
            "bindings.list.start",
            message=f"Listing bindings for vhost={request.vhost}",
            operation="bindings.list",
            vhost=request.vhost,
            filters={
                "source": request.source,
                "destination": request.destination,
                "destination_type": request.destination_type,
            },
            sort_by=request.sort_by,
            correlation_id=correlation_id,
        )

        try:
            # Perform GET request (AC #1, Task 3)
            response = await self.http_client.request(
                method="GET",
                path=endpoint,
            )

            # 200 OK with JSON array (AC #1, #3, #4)
            if response.status_code == 200:
                duration_ms = (time.time() - start_time) * 1000

                # Parse response array into List[BindingInfo]
                bindings_data = response.json()

                # Handle empty list (AC #6)
                if not bindings_data:
                    logger.info(
                        "bindings.list.empty",
                        message=f"No bindings found in vhost '{request.vhost}'",
                        operation="bindings.list",
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )
                    return []

                # Validate each binding using Pydantic (AC #1, #3, #4, Task 2)
                bindings = [BindingInfo(**b) for b in bindings_data]

                # Apply client-side filters (AC #2, Task 4)
                filtered_bindings = self._apply_filters(
                    bindings,
                    request.source,
                    request.destination,
                    request.destination_type,
                )

                # Log filtering results if filters were applied
                if request.source or request.destination or request.destination_type:
                    logger.info(
                        "bindings.list.filtered",
                        message=f"Filtered bindings: {len(bindings)} → {len(filtered_bindings)}",
                        operation="bindings.list",
                        vhost=request.vhost,
                        filters={
                            "source": request.source,
                            "destination": request.destination,
                            "destination_type": request.destination_type,
                        },
                        original_count=len(bindings),
                        filtered_count=len(filtered_bindings),
                        correlation_id=correlation_id,
                    )

                # Apply client-side sorting (AC #7, Task 4)
                sorted_bindings = self._apply_sorting(
                    filtered_bindings,
                    request.sort_by,
                )

                # Log successful list (Task 6)
                logger.info(
                    "bindings.list.success",
                    message=f"Listed {len(sorted_bindings)} bindings in vhost '{request.vhost}'",
                    operation="bindings.list",
                    vhost=request.vhost,
                    binding_count=len(sorted_bindings),
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                # Performance monitoring (AC #5, Task 7)
                if duration_ms > 200:
                    logger.warning(
                        "bindings.list.slow",
                        message="Bindings list operation exceeded 200ms threshold",
                        operation="bindings.list",
                        vhost=request.vhost,
                        duration_ms=duration_ms,
                        threshold_ms=200,
                        binding_count=len(sorted_bindings),
                        correlation_id=correlation_id,
                    )

                return sorted_bindings

            # 404 Not Found - vhost doesn't exist (caught by VhostValidator)
            elif response.status_code == 404:
                logger.warning(
                    "bindings.list.vhost_not_found",
                    message=f"Virtual host '{request.vhost}' does not exist",
                    operation="bindings.list",
                    vhost=request.vhost,
                    status_code=response.status_code,
                    correlation_id=correlation_id,
                )
                raise ValueError(f"Virtual host '{request.vhost}' does not exist")

            # Unexpected status code
            else:
                logger.error(
                    "bindings.list.unexpected_status",
                    message=f"Unexpected status code: {response.status_code}",
                    operation="bindings.list",
                    vhost=request.vhost,
                    status_code=response.status_code,
                    correlation_id=correlation_id,
                )
                raise ValueError(
                    f"Failed to list bindings: HTTP {response.status_code}"
                )

        except httpx.TimeoutException:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "bindings.list.timeout",
                message=f"List bindings request timed out after {duration_ms:.0f}ms",
                operation="bindings.list",
                vhost=request.vhost,
                duration_ms=duration_ms,
                correlation_id=correlation_id,
            )
            raise

        except ValueError:
            # Re-raise validation errors (vhost not found, etc.)
            raise

        except Exception as e:
            logger.error(
                "bindings.list.error",
                message=f"Unexpected error during list: {str(e)}",
                operation="bindings.list",
                vhost=request.vhost,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

    def _apply_filters(
        self,
        bindings: list[BindingInfo],
        source: str | None,
        destination: str | None,
        destination_type: str | None,
    ) -> list[BindingInfo]:
        """Apply client-side filters to binding list (AC #2, Task 4).

        Args:
            bindings: List of BindingInfo to filter
            source: Filter by source exchange name (optional)
            destination: Filter by destination queue/exchange name (optional)
            destination_type: Filter by destination type (optional)

        Returns:
            Filtered list of BindingInfo
        """
        filtered = bindings

        # Filter by source exchange (exact match)
        if source:
            filtered = [b for b in filtered if b.source == source]

        # Filter by destination (exact match)
        if destination:
            filtered = [b for b in filtered if b.destination == destination]

        # Filter by destination_type (queue or exchange)
        if destination_type:
            filtered = [b for b in filtered if b.destination_type == destination_type]

        return filtered

    def _apply_sorting(
        self,
        bindings: list[BindingInfo],
        sort_by: str | None,
    ) -> list[BindingInfo]:
        """Apply client-side sorting to binding list (AC #7, Task 4).

        Args:
            bindings: List of BindingInfo to sort
            sort_by: Sort field (source, destination, routing_key) or None for API order

        Returns:
            Sorted list of BindingInfo
        """
        if sort_by == "source":
            # Alphabetical sort by source exchange
            return sorted(bindings, key=lambda b: b.source.lower())

        elif sort_by == "destination":
            # Alphabetical sort by destination
            return sorted(bindings, key=lambda b: b.destination.lower())

        elif sort_by == "routing_key":
            # Alphabetical sort by routing_key
            return sorted(bindings, key=lambda b: b.routing_key.lower())

        else:
            # No sorting - return in API order
            return bindings


class CreateBindingOperation:
    """Create binding operation with validation and idempotency (Story 3.9, AC #1-9).

    Implements async binding creation between exchange and queue (or exchange and exchange)
    using HTTPClientManager to call RabbitMQ Management API POST /api/bindings endpoints.
    Includes pre-creation validation, idempotency handling (204 No Content), and
    comprehensive error handling with clear messages.

    Binding Types:
        - Exchange-to-Queue (destination_type="queue"): Most common binding for routing
        - Exchange-to-Exchange (destination_type="exchange"): Federation patterns

    Routing Key Validation by Exchange Type:
        - Fanout: routing_key ignored (empty string allowed)
        - Direct: exact match only (no wildcards)
        - Topic: wildcards * (one word) and # (zero or more words)
        - Headers: routing_key optional, uses arguments with x-match rules
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize CreateBindingOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client

    @validate_vhost
    async def execute(
        self,
        request: CreateBindingRequest,
        correlation_id: str | None = None,
    ) -> BindingInfo:
        """Execute create binding operation with validation and idempotency (AC #1-9).

        Args:
            request: CreateBindingRequest with binding configuration
            correlation_id: Correlation ID for audit trail (optional, ADR-009)

        Returns:
            BindingInfo model with created binding details

        Raises:
            ValueError: If source/destination not found or validation fails (AC #5, #6, #7)
            httpx.TimeoutException: If request times out (AC #8)
        """
        start_time = time.time()

        # URL encode vhost, source, and destination (Architecture Constraints)
        encoded_vhost = quote(request.vhost, safe="")
        encoded_source = quote(request.source, safe="")
        encoded_destination = quote(request.destination, safe="")

        logger.info(
            "bindings.create.start",
            message=f"Creating binding from '{request.source}' to '{request.destination}' in vhost '{request.vhost}'",
            operation="bindings.create",
            source=request.source,
            destination=request.destination,
            destination_type=request.destination_type,
            routing_key=request.routing_key,
            vhost=request.vhost,
            arguments=request.arguments,
            correlation_id=correlation_id,
        )

        try:
            # Pre-creation validation (AC #5, #6, #7, Task 3)
            await self._validate_binding_preconditions(request, correlation_id)

            # Construct endpoint based on destination_type (AC #1, #2)
            if request.destination_type == "queue":
                endpoint = f"/api/bindings/{encoded_vhost}/e/{encoded_source}/q/{encoded_destination}"
            else:  # exchange
                endpoint = f"/api/bindings/{encoded_vhost}/e/{encoded_source}/e/{encoded_destination}"

            # Prepare request body (AC #1, #3)
            body: dict[str, Any] = {
                "routing_key": request.routing_key,
                "arguments": request.arguments or {},
            }

            # Create binding via POST request (AC #1)
            response = await self.http_client.request("POST", endpoint, json=body)

            # Handle 201 Created (success - new binding created)
            if response.status_code == 201:
                # Calculate duration for performance monitoring
                duration_ms = (time.time() - start_time) * 1000

                # Retrieve binding details from Location header or fetch
                binding_info = await self._get_binding_details(
                    encoded_vhost,
                    encoded_source,
                    encoded_destination,
                    request.destination_type,
                    request.routing_key,
                    correlation_id,
                )

                if not binding_info:
                    logger.error(
                        "bindings.create.verification_failed",
                        message="Binding created but could not retrieve details",
                        operation="bindings.create",
                        source=request.source,
                        destination=request.destination,
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )
                    raise ValueError(
                        f"Binding from '{request.source}' to '{request.destination}' created but verification failed"
                    )

                # Log successful creation (AC #9)
                logger.info(
                    "bindings.create.success",
                    message=f"Binding created successfully from '{request.source}' to '{request.destination}'",
                    operation="bindings.create",
                    source=request.source,
                    destination=request.destination,
                    destination_type=request.destination_type,
                    routing_key=request.routing_key,
                    vhost=request.vhost,
                    duration_ms=round(duration_ms, 2),
                    idempotent=False,
                    correlation_id=correlation_id,
                )

                # Log warning if operation exceeds 100ms threshold (AC #8, Task 7)
                if duration_ms > 100:
                    logger.warning(
                        "bindings.create.slow",
                        message=f"Binding creation exceeded latency threshold: {duration_ms:.2f}ms",
                        operation="bindings.create",
                        duration_ms=round(duration_ms, 2),
                        threshold_ms=100,
                        source=request.source,
                        destination=request.destination,
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )

                return binding_info

            # Handle 204 No Content (idempotent - binding already exists with same properties)
            elif response.status_code == 204:
                duration_ms = (time.time() - start_time) * 1000

                # Retrieve existing binding details (AC #4, Task 4)
                binding_info = await self._get_binding_details(
                    encoded_vhost,
                    encoded_source,
                    encoded_destination,
                    request.destination_type,
                    request.routing_key,
                    correlation_id,
                )

                if not binding_info:
                    logger.warning(
                        "bindings.create.idempotent_verification_failed",
                        message="Idempotent binding but could not retrieve details",
                        operation="bindings.create",
                        source=request.source,
                        destination=request.destination,
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )
                    # Create BindingInfo from request parameters as fallback
                    binding_info = BindingInfo(
                        source=request.source,
                        destination=request.destination,
                        destination_type=request.destination_type,
                        routing_key=request.routing_key,
                        vhost=request.vhost,
                        arguments=request.arguments,
                    )

                # Log idempotent success (AC #4, #9, Task 4)
                logger.info(
                    "bindings.create.idempotent",
                    message=f"Binding already exists from '{request.source}' to '{request.destination}' (idempotent success)",
                    operation="bindings.create",
                    source=request.source,
                    destination=request.destination,
                    destination_type=request.destination_type,
                    routing_key=request.routing_key,
                    vhost=request.vhost,
                    duration_ms=round(duration_ms, 2),
                    idempotent=True,
                    correlation_id=correlation_id,
                )

                return binding_info

            # Handle 404 Not Found (source exchange or destination not found)
            elif response.status_code == 404:
                await self._handle_not_found_error(
                    response,
                    request.source,
                    request.destination,
                    request.destination_type,
                    request.vhost,
                    correlation_id,
                )

            # Handle 400 Bad Request (validation error - invalid routing key or arguments)
            elif response.status_code == 400:
                error_detail = "Invalid request"
                try:
                    error_data = response.json()
                    error_detail = error_data.get("reason", error_detail)
                except Exception:
                    error_detail = response.text or error_detail

                logger.error(
                    "bindings.create.validation_error",
                    message=f"Validation error creating binding: {error_detail}",
                    operation="bindings.create",
                    source=request.source,
                    destination=request.destination,
                    routing_key=request.routing_key,
                    vhost=request.vhost,
                    error=error_detail,
                    correlation_id=correlation_id,
                )
                raise ValueError(
                    f"Validation error creating binding from '{request.source}' to '{request.destination}': {error_detail}"
                )

            # Handle unexpected status codes
            else:
                error_msg = (
                    f"Unexpected response creating binding: HTTP {response.status_code}"
                )
                logger.error(
                    "bindings.create.error",
                    message=error_msg,
                    operation="bindings.create",
                    source=request.source,
                    destination=request.destination,
                    vhost=request.vhost,
                    status_code=response.status_code,
                    correlation_id=correlation_id,
                )
                raise ValueError(error_msg)

        except httpx.TimeoutException:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "bindings.create.timeout",
                message=f"Binding creation timed out after {duration_ms:.2f}ms",
                operation="bindings.create",
                source=request.source,
                destination=request.destination,
                vhost=request.vhost,
                duration_ms=round(duration_ms, 2),
                correlation_id=correlation_id,
            )
            raise
        except ValueError:
            # Re-raise validation errors from pre-creation checks or response handling
            raise
        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "bindings.create.error",
                message=f"Unexpected error creating binding: {str(e)}",
                operation="bindings.create",
                source=request.source,
                destination=request.destination,
                vhost=request.vhost,
                duration_ms=round(duration_ms, 2),
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

    async def _validate_binding_preconditions(
        self,
        request: CreateBindingRequest,
        correlation_id: str | None = None,
    ) -> None:
        """Validate binding pre-creation conditions (AC #5, #6, #7, Task 3).

        Verifies source exchange and destination resource exist before attempting
        binding creation. Retrieves exchange type for routing key validation.

        Args:
            request: CreateBindingRequest with binding parameters
            correlation_id: Correlation ID for audit trail

        Raises:
            ValueError: If source exchange or destination not found, or routing key invalid
        """
        encoded_vhost = quote(request.vhost, safe="")
        encoded_source = quote(request.source, safe="")
        encoded_destination = quote(request.destination, safe="")

        # Verify source exchange exists (AC #5)
        try:
            source_endpoint = f"/api/exchanges/{encoded_vhost}/{encoded_source}"
            source_response = await self.http_client.request("GET", source_endpoint)

            if source_response.status_code == 404:
                logger.error(
                    "bindings.create.not_found",
                    message=f"Source exchange '{request.source}' not found in vhost '{request.vhost}'",
                    operation="bindings.create",
                    resource_type="source",
                    resource_name=request.source,
                    vhost=request.vhost,
                    correlation_id=correlation_id,
                )
                raise ValueError(
                    f"Source exchange '{request.source}' not found in vhost '{request.vhost}'. "
                    f"Please create the exchange first."
                )

            if source_response.status_code != 200:
                raise ValueError(
                    f"Failed to verify source exchange '{request.source}': HTTP {source_response.status_code}"
                )

            # Retrieve exchange type for routing key validation
            source_data = source_response.json()
            exchange_type = source_data.get("type")

            # Validate routing key based on exchange type (AC #7)
            self._validate_routing_key(
                request.routing_key, exchange_type, request.source
            )

        except ValueError:
            raise
        except Exception as e:
            logger.error(
                "bindings.create.validation_error",
                message=f"Error verifying source exchange: {str(e)}",
                operation="bindings.create",
                source=request.source,
                vhost=request.vhost,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise ValueError(
                f"Error verifying source exchange '{request.source}': {str(e)}"
            )

        # Verify destination exists (AC #6)
        try:
            if request.destination_type == "queue":
                dest_endpoint = f"/api/queues/{encoded_vhost}/{encoded_destination}"
            else:  # exchange
                dest_endpoint = f"/api/exchanges/{encoded_vhost}/{encoded_destination}"

            dest_response = await self.http_client.request("GET", dest_endpoint)

            if dest_response.status_code == 404:
                logger.error(
                    "bindings.create.not_found",
                    message=f"Destination {request.destination_type} '{request.destination}' not found in vhost '{request.vhost}'",
                    operation="bindings.create",
                    resource_type="destination",
                    resource_name=request.destination,
                    vhost=request.vhost,
                    correlation_id=correlation_id,
                )
                raise ValueError(
                    f"Destination {request.destination_type} '{request.destination}' not found in vhost '{request.vhost}'. "
                    f"Please create the {request.destination_type} first."
                )

            if dest_response.status_code != 200:
                raise ValueError(
                    f"Failed to verify destination {request.destination_type} '{request.destination}': HTTP {dest_response.status_code}"
                )

        except ValueError:
            raise
        except Exception as e:
            logger.error(
                "bindings.create.validation_error",
                message=f"Error verifying destination {request.destination_type}: {str(e)}",
                operation="bindings.create",
                destination=request.destination,
                destination_type=request.destination_type,
                vhost=request.vhost,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise ValueError(
                f"Error verifying destination {request.destination_type} '{request.destination}': {str(e)}"
            )

    def _validate_routing_key(
        self, routing_key: str, exchange_type: str | None, exchange_name: str
    ) -> None:
        """Validate routing key based on exchange type (AC #7, Task 3).

        Args:
            routing_key: Routing key pattern to validate
            exchange_type: Exchange type (fanout, direct, topic, headers)
            exchange_name: Exchange name for error messages

        Raises:
            ValueError: If routing key is invalid for the exchange type
        """
        if not exchange_type:
            # Cannot validate without exchange type
            return

        # Fanout: routing_key ignored (can be empty)
        if exchange_type == "fanout":
            return

        # Direct: no wildcards allowed (exact match only)
        if exchange_type == "direct":
            if "*" in routing_key or "#" in routing_key:
                raise ValueError(
                    f"Direct exchange '{exchange_name}' does not support wildcards in routing key. "
                    f"Use exact match only (no * or # allowed)."
                )
            return

        # Topic: allow * (one word) and # (zero or more words) with . separators
        if exchange_type == "topic":
            # Validate topic pattern: words separated by dots, can contain * or #
            # Allow empty routing key for topic exchanges (matches nothing)
            if routing_key:
                # Check for invalid characters (only alphanumeric, ., *, #, -, _ allowed)
                if not re.match(r"^[a-zA-Z0-9.*#_-]+$", routing_key):
                    raise ValueError(
                        f"Topic exchange routing key '{routing_key}' contains invalid characters. "
                        f"Only alphanumeric, ., *, #, -, _ are allowed."
                    )
            return

        # Headers: routing_key optional, uses arguments for matching
        if exchange_type == "headers":
            return

        # Other exchange types: no specific validation

    async def _get_binding_details(
        self,
        encoded_vhost: str,
        encoded_source: str,
        encoded_destination: str,
        destination_type: str,
        routing_key: str,
        correlation_id: str | None = None,
    ) -> BindingInfo | None:
        """Retrieve binding details after creation (Task 2).

        Args:
            encoded_vhost: URL-encoded vhost name
            encoded_source: URL-encoded source exchange name
            encoded_destination: URL-encoded destination name
            destination_type: "queue" or "exchange"
            routing_key: Binding routing key
            correlation_id: Correlation ID for audit trail

        Returns:
            BindingInfo if found, None otherwise
        """
        try:
            # List bindings for this source-destination pair
            if destination_type == "queue":
                endpoint = f"/api/bindings/{encoded_vhost}/e/{encoded_source}/q/{encoded_destination}"
            else:
                endpoint = f"/api/bindings/{encoded_vhost}/e/{encoded_source}/e/{encoded_destination}"

            response = await self.http_client.request("GET", endpoint)

            if response.status_code != 200:
                return None

            bindings_data = response.json()

            # Find binding with matching routing key
            for binding_data in bindings_data:
                if binding_data.get("routing_key") == routing_key:
                    return BindingInfo(
                        source=binding_data.get("source"),
                        destination=binding_data.get("destination"),
                        destination_type=binding_data.get(
                            "destination_type", destination_type
                        ),
                        routing_key=binding_data.get("routing_key"),
                        vhost=binding_data.get("vhost"),
                        arguments=binding_data.get("arguments"),
                        properties_key=binding_data.get("properties_key"),
                    )

            return None

        except Exception as e:
            logger.warning(
                "bindings.create.fetch_details_failed",
                message=f"Could not fetch binding details: {str(e)}",
                operation="bindings.create",
                error=str(e),
                correlation_id=correlation_id,
            )
            return None

    async def _handle_not_found_error(
        self,
        response: httpx.Response,
        source: str,
        destination: str,
        destination_type: str,
        vhost: str,
        correlation_id: str | None = None,
    ) -> NoReturn:
        """Handle 404 Not Found errors with user-friendly messages (AC #5, #6, Task 5).

        Args:
            response: HTTP response with 404 status
            source: Source exchange name
            destination: Destination name
            destination_type: "queue" or "exchange"
            vhost: Virtual host name
            correlation_id: Correlation ID for audit trail

        Raises:
            ValueError: With clear error message indicating missing resource
        """
        error_detail = "Resource not found"
        try:
            error_data = response.json()
            error_detail = error_data.get("reason", error_detail)
        except Exception:
            error_detail = response.text or error_detail

        # Determine if source or destination is missing from error message
        if (
            "exchange" in error_detail.lower()
            and source.lower() in error_detail.lower()
        ):
            logger.error(
                "bindings.create.not_found",
                message=f"Source exchange '{source}' not found",
                operation="bindings.create",
                resource_type="source",
                resource_name=source,
                vhost=vhost,
                error=error_detail,
                correlation_id=correlation_id,
            )
            raise ValueError(
                f"Source exchange '{source}' not found in vhost '{vhost}'. "
                f"Please create the exchange first."
            )
        elif destination.lower() in error_detail.lower():
            logger.error(
                "bindings.create.not_found",
                message=f"Destination {destination_type} '{destination}' not found",
                operation="bindings.create",
                resource_type="destination",
                resource_name=destination,
                vhost=vhost,
                error=error_detail,
                correlation_id=correlation_id,
            )
            raise ValueError(
                f"Destination {destination_type} '{destination}' not found in vhost '{vhost}'. "
                f"Please create the {destination_type} first."
            )
        else:
            # Generic 404 error
            logger.error(
                "bindings.create.not_found",
                message=f"Resource not found: {error_detail}",
                operation="bindings.create",
                source=source,
                destination=destination,
                vhost=vhost,
                error=error_detail,
                correlation_id=correlation_id,
            )
            raise ValueError(
                f"Resource not found creating binding from '{source}' to '{destination}': {error_detail}"
            )


class DeleteBindingOperation:
    """Delete binding operation with safety and audit trail (Story 3.10, AC #1-7).

    Implements async binding deletion between exchange and queue (or exchange and exchange)
    using HTTPClientManager to call RabbitMQ Management API DELETE /api/bindings endpoints.
    Includes properties_key retrieval for exact binding identification, comprehensive error
    handling, and performance monitoring.

    Binding Types:
        - Exchange-to-Queue (destination_type="queue"): Delete binding from exchange to queue
        - Exchange-to-Exchange (destination_type="exchange"): Delete binding between exchanges

    Safety Notes:
        - Deleting a binding does NOT delete the source exchange or destination
        - Multiple bindings can exist between same source/destination with different routing keys
        - This operation only deletes the exact match identified by routing_key
        - No confirmation required (bindings don't store data, low risk operation)
    """

    def __init__(self, http_client: HTTPClientManager) -> None:
        """Initialize DeleteBindingOperation.

        Args:
            http_client: HTTPClientManager instance for API calls
        """
        self.http_client = http_client
        self.list_bindings_operation = ListBindingsOperation(http_client)

    @validate_vhost
    async def execute(
        self,
        request: DeleteBindingRequest,
        correlation_id: str | None = None,
    ) -> DeleteBindingResponse:
        """Execute delete binding operation (AC #1, #2, #3, #4).

        Args:
            request: DeleteBindingRequest with binding identification
            correlation_id: Correlation ID for audit trail (ADR-009)

        Returns:
            DeleteBindingResponse with status and message

        Raises:
            ValueError: If binding not found (AC #4)
            httpx.TimeoutException: If request times out (AC #5)
        """
        start_time = time.time()

        # URL encode vhost, source, and destination (Architecture Constraints)
        encoded_vhost = quote(request.vhost, safe="")
        encoded_source = quote(request.source, safe="")
        encoded_destination = quote(request.destination, safe="")

        logger.info(
            "bindings.delete.start",
            message=f"Deleting binding from '{request.source}' to '{request.destination}' in vhost '{request.vhost}'",
            operation="bindings.delete",
            source=request.source,
            destination=request.destination,
            destination_type=request.destination_type,
            routing_key=request.routing_key,
            vhost=request.vhost,
            correlation_id=correlation_id,
        )

        try:
            # Retrieve properties_key for exact binding identification (AC #4, Task 3)
            properties_key = await self._find_binding_properties_key(
                request, correlation_id
            )

            # Construct endpoint based on destination_type (AC #1)
            if request.destination_type == "queue":
                endpoint = f"/api/bindings/{encoded_vhost}/e/{encoded_source}/q/{encoded_destination}/{properties_key}"
            else:  # exchange
                endpoint = f"/api/bindings/{encoded_vhost}/e/{encoded_source}/e/{encoded_destination}/{properties_key}"

            # Delete binding via DELETE request (AC #1)
            response = await self.http_client.request("DELETE", endpoint)

            # Handle 204 No Content (success - binding deleted)
            if response.status_code == 204:
                # Calculate duration for performance monitoring
                duration_ms = (time.time() - start_time) * 1000

                logger.info(
                    "bindings.delete.success",
                    message=f"Binding deleted from '{request.source}' to '{request.destination}'",
                    operation="bindings.delete",
                    source=request.source,
                    destination=request.destination,
                    routing_key=request.routing_key,
                    vhost=request.vhost,
                    duration_ms=duration_ms,
                    correlation_id=correlation_id,
                )

                # Performance monitoring: warn if exceeds threshold (AC #5, Task 6)
                if duration_ms > 100:
                    logger.warning(
                        "bindings.delete.slow",
                        message=f"Binding deletion exceeded threshold ({duration_ms:.2f}ms > 100ms)",
                        operation="bindings.delete",
                        duration_ms=duration_ms,
                        threshold_ms=100,
                        source=request.source,
                        destination=request.destination,
                        vhost=request.vhost,
                        correlation_id=correlation_id,
                    )

                return DeleteBindingResponse(
                    status="success",
                    message="Binding deleted",
                    source=request.source,
                    destination=request.destination,
                    routing_key=request.routing_key,
                    vhost=request.vhost,
                    destination_type=request.destination_type,
                    correlation_id=correlation_id or "",
                    duration_ms=duration_ms,
                )

            # Handle 404 Not Found (binding doesn't exist)
            elif response.status_code == 404:
                return self._handle_binding_not_found(request, correlation_id)

            # Handle unexpected responses
            else:
                error_msg = f"Unexpected response status: {response.status_code}"
                logger.error(
                    "bindings.delete.error",
                    message=error_msg,
                    operation="bindings.delete",
                    status_code=response.status_code,
                    source=request.source,
                    destination=request.destination,
                    vhost=request.vhost,
                    correlation_id=correlation_id,
                )
                raise ValueError(error_msg)

        except ValueError:
            # Re-raise ValueError (already handled with proper message)
            raise

        except httpx.TimeoutException:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "bindings.delete.timeout",
                message=f"Request timeout deleting binding ({duration_ms:.2f}ms)",
                operation="bindings.delete",
                source=request.source,
                destination=request.destination,
                vhost=request.vhost,
                duration_ms=duration_ms,
                correlation_id=correlation_id,
            )
            raise

        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(
                "bindings.delete.error",
                message=f"Error deleting binding: {str(e)}",
                operation="bindings.delete",
                source=request.source,
                destination=request.destination,
                vhost=request.vhost,
                duration_ms=duration_ms,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise

    async def _find_binding_properties_key(
        self,
        request: DeleteBindingRequest,
        correlation_id: str | None = None,
    ) -> str:
        """Find properties_key for exact binding identification (AC #4, Task 3).

        Args:
            request: DeleteBindingRequest with binding details
            correlation_id: Correlation ID for audit trail

        Returns:
            properties_key for the matching binding

        Raises:
            ValueError: If binding not found
        """
        # List all bindings for source-destination pair
        list_request = ListBindingsRequest(
            source=request.source,
            destination=request.destination,
            destination_type=request.destination_type,
            vhost=request.vhost,
        )

        try:
            bindings = await self.list_bindings_operation.execute(
                list_request, correlation_id
            )
        except Exception as e:
            logger.error(
                "bindings.delete.list_error",
                message=f"Error listing bindings for properties_key retrieval: {str(e)}",
                operation="bindings.delete",
                source=request.source,
                destination=request.destination,
                vhost=request.vhost,
                error=str(e),
                correlation_id=correlation_id,
            )
            raise ValueError(
                f"Failed to list bindings for '{request.source}' to '{request.destination}': {str(e)}"
            )

        # Filter by exact routing_key match
        matching_bindings = [
            binding
            for binding in bindings
            if binding.routing_key == request.routing_key
        ]

        if not matching_bindings:
            # Binding not found - return detailed error
            raise ValueError(
                f"Binding not found: source='{request.source}', "
                f"destination='{request.destination}', "
                f"routing_key='{request.routing_key}' in vhost '{request.vhost}'"
            )

        # Return properties_key from matching binding
        binding = matching_bindings[0]
        properties_key = binding.properties_key

        if properties_key is None:
            raise ValueError(
                f"Binding found but missing properties_key: source='{request.source}', "
                f"destination='{request.destination}', routing_key='{request.routing_key}'"
            )

        logger.debug(
            "bindings.delete.properties_key_found",
            message=f"Found properties_key '{properties_key}' for binding",
            operation="bindings.delete",
            source=request.source,
            destination=request.destination,
            routing_key=request.routing_key,
            properties_key=properties_key,
            vhost=request.vhost,
            correlation_id=correlation_id,
        )

        return properties_key

    def _handle_binding_not_found(
        self,
        request: DeleteBindingRequest,
        correlation_id: str | None = None,
    ) -> NoReturn:
        """Handle 404 not found error for binding deletion (AC #4, Task 4).

        Args:
            request: DeleteBindingRequest with binding details
            correlation_id: Correlation ID for audit trail

        Raises:
            ValueError: Always raises with user-friendly error message
        """
        error_message = (
            f"Binding not found: source='{request.source}', "
            f"destination='{request.destination}', "
            f"routing_key='{request.routing_key}' in vhost '{request.vhost}'. "
            f"Verify binding exists before deletion."
        )

        logger.warning(
            "bindings.delete.not_found",
            message=error_message,
            operation="bindings.delete",
            source=request.source,
            destination=request.destination,
            routing_key=request.routing_key,
            vhost=request.vhost,
            correlation_id=correlation_id,
        )

        raise ValueError(error_message)
