"""Buffered OTLP exporters with automatic retry and queueing.

This module provides wrapped OTLP exporters that queue telemetry when
the collector is unreachable and automatically retry with exponential backoff.
"""

import logging
import threading
import time
from typing import Any, Optional, Protocol, Sequence, runtime_checkable

from opentelemetry.sdk.metrics.export import (
    MetricsData,
    MetricExportResult,
)
from opentelemetry.sdk._logs._internal import ReadableLogRecord
from opentelemetry.sdk._logs._internal.export import LogRecordExportResult
from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace.export import SpanExportResult
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

from .queue import TelemetryQueue
from .retry import RetryPolicy
from .queued_item import QueuedItem
from ..utils.exceptions import BufferingError

# Circuit breaker (Story 3-3)
from pybreaker import CircuitBreakerError

logger = logging.getLogger(__name__)


@runtime_checkable
class MeterSettable(Protocol):
    """Protocol for objects that accept meter injection for telemetry.

    This protocol defines the interface for components that can emit their
    own internal metrics when given a meter instance. Used for circuit breakers
    and buffered exporters to report their operational metrics.
    """

    def set_meter(self, meter) -> None:
        """Set meter for emitting internal metrics.

        Args:
            meter: OpenTelemetry Meter instance for creating metrics
        """
        ...


class BufferedOTLPExporterBase:
    """Base class for buffered OTLP exporters with common worker loop logic.

    Provides shared implementation of the background worker thread that drains
    the queue and retries failed exports. Subclasses must implement abstract
    methods to define export behavior and success criteria.

    Attributes that subclasses must initialize:
        _running: bool - Whether background worker is running
        _lock: threading.Lock - Lock for thread-safe operations
        _worker_thread: Optional[threading.Thread] - Background worker thread
        _queue: TelemetryQueue - Queue for buffering telemetry
        _drain_interval: float - Seconds between queue drain attempts
        _max_retry_attempts: int - Maximum retry attempts per item
        _max_retry_queue_size: int - Maximum queue size
        _export_successes: int - Counter for successful exports
        _items_dropped: int - Counter for dropped items
        _items_requeued: int - Counter for requeued items
    """

    def _worker_loop(self) -> None:
        """Background worker loop (internal method).

        Periodically drains queue and attempts to export telemetry.
        Runs until stop_background_worker() is called.
        """
        logger.debug("Background worker loop started")

        while self._running:
            try:
                batch_size = min(10, self._queue.size())
                if batch_size > 0:
                    batch = self._queue.dequeue_batch(batch_size)
                    logger.debug(
                        f"Draining {len(batch)} items from queue", extra={"batch_size": len(batch)}
                    )

                    for item in batch:
                        self._process_queued_item(item)

            except Exception as e:
                logger.error(f"Error in worker loop: {e}", extra={"error": str(e)})

            # Emit persistence metrics (Story 3-4)
            try:
                self._emit_persistence_metrics()
            except Exception as e:
                logger.error(f"Error emitting persistence metrics: {e}")

            # Sleep until next drain
            time.sleep(self._drain_interval)

        logger.debug("Background worker loop stopped")

    def _process_queued_item(self, item) -> None:
        """Process a single queued item (export with retry).

        Args:
            item: QueuedItem or raw data to export
        """
        # Unwrap QueuedItem (with backward compatibility for raw data)
        if isinstance(item, QueuedItem):
            queued_item = item
            data = item.data
        else:
            queued_item = QueuedItem(data=item, attempt_count=0)
            data = item

        # Check if exceeded attempt limit - drop if so
        if queued_item.attempt_count >= self._max_retry_attempts:
            logger.warning(
                f"Dropping item after {queued_item.attempt_count} attempts",
                extra={
                    "attempt_count": queued_item.attempt_count,
                    "first_queued_at": queued_item.first_queued_at,
                    "last_error": queued_item.last_error,
                },
            )
            self._items_dropped += 1
            return

        # Increment attempt counter BEFORE retry
        queued_item.attempt_count += 1

        try:
            result = self._export_with_retry(data)

            if self._is_success(result):
                self._export_successes += 1
                logger.debug("Queued item exported successfully")
            else:
                # Failed again, re-queue with incremented counter
                queued_item.last_error = str(result)
                self._items_requeued += 1
                if self._queue.size() < self._max_retry_queue_size:
                    self._queue.enqueue(queued_item)
                    logger.warning(f"Re-queued failed item: {result}")
                else:
                    logger.warning("Dropped item: queue limit reached")

        except Exception as e:
            # Re-queue on exception with incremented counter
            queued_item.last_error = str(e)
            self._items_requeued += 1
            if self._queue.size() < self._max_retry_queue_size:
                self._queue.enqueue(queued_item)
                logger.warning(f"Re-queued item after exception: {e}")

    def _export_with_retry(self, data):
        """Export data with retry policy (subclass implements).

        Args:
            data: Data to export

        Returns:
            Export result from underlying exporter
        """
        raise NotImplementedError("Subclass must implement _export_with_retry")

    def _is_success(self, result) -> bool:
        """Check if export result indicates success (subclass implements).

        Args:
            result: Export result from underlying exporter

        Returns:
            True if export was successful, False otherwise
        """
        raise NotImplementedError("Subclass must implement _is_success")

    def start_background_worker(self) -> None:
        """Start background worker thread to drain queue.

        The worker periodically attempts to export queued telemetry.
        Safe to call multiple times (no-op if already running).

        Example:
            >>> exporter.start_background_worker()
            >>> # Worker now drains queue every drain_interval seconds
        """
        with self._lock:
            if self._running:
                logger.debug("Background worker already running")
                return

            self._running = True
            self._worker_thread = threading.Thread(
                target=self._worker_loop, name="BufferedExporter-Worker", daemon=True
            )
            self._worker_thread.start()
            logger.info("Started background worker thread")

    def stop_background_worker(self) -> None:
        """Stop background worker thread.

        Waits for worker to finish current operation before stopping.
        Safe to call multiple times (no-op if not running).

        Example:
            >>> exporter.stop_background_worker()
        """
        with self._lock:
            if not self._running:
                return

            self._running = False

        # Wait for worker thread to finish
        if self._worker_thread and self._worker_thread.is_alive():
            self._worker_thread.join(timeout=5.0)
            logger.info("Stopped background worker thread")

    def _emit_persistence_metrics(self) -> None:
        """Emit persistence metrics if meter is set (subclass can override).

        Default implementation does nothing. Subclasses with persistence support
        should override this method to emit metrics.
        """
        pass


class BufferedOTLPMetricExporter(BufferedOTLPExporterBase):
    """Buffered wrapper for OTLP metric exporter.

    Wraps the standard OTLPMetricExporter to provide automatic queueing
    and retry logic when the collector is temporarily unreachable.

    Features:
        - Automatic queueing of failed exports
        - Background worker thread drains queue periodically
        - Retry logic with exponential backoff
        - Graceful shutdown with final flush
        - Prevents infinite queue growth on persistent failures

    Thread Safety:
        All operations are thread-safe. The background worker runs in
        a separate daemon thread.

    Examples:
        >>> queue = TelemetryQueue(max_size=1000)
        >>> retry_policy = RetryPolicy(max_attempts=3)
        >>> exporter = BufferedOTLPMetricExporter(
        ...     endpoint="http://localhost:4318/v1/metrics",
        ...     queue=queue,
        ...     retry_policy=retry_policy
        ... )
        >>> # Exporter will automatically queue and retry failed exports
    """

    def __init__(
        self,
        endpoint: str = "http://localhost:4318/v1/metrics",
        headers: Optional[dict] = None,
        timeout: int = 10,
        queue: Optional[TelemetryQueue] = None,
        retry_policy: Optional[RetryPolicy] = None,
        drain_interval: float = 1.0,
        max_retry_queue_size: int = 100,
        max_retry_attempts: int = 3,
        persist_queue: bool = False,
        persist_path: Optional[str] = None,
        persist_min_disk_space_mb: float = 10.0,
        config: Optional[Any] = None,
    ):
        """Initialize buffered metric exporter.

        Args:
            endpoint: OTLP collector endpoint URL
            headers: Optional HTTP headers for requests
            timeout: Request timeout in seconds
            queue: TelemetryQueue for buffering (creates default if None)
            retry_policy: RetryPolicy for retries (creates default if None)
            drain_interval: Seconds between queue drain attempts (default: 1.0)
            max_retry_queue_size: Max items to re-queue on failure (default: 100)
            max_retry_attempts: Max dequeue attempts before dropping item (default: 3)
            persist_queue: Whether to persist queue to disk (default: False)
            persist_path: Path for queue persistence (required if persist_queue=True)
            persist_min_disk_space_mb: Minimum free disk space (MB) for persistence (default: 10.0)
            config: Optional MCAConfig for custom allowed directories (Story 3.15)

        Raises:
            BufferingError: If configuration is invalid
        """
        if drain_interval <= 0:
            raise BufferingError(f"drain_interval must be positive, got {drain_interval}")

        if max_retry_queue_size <= 0:
            raise BufferingError(
                f"max_retry_queue_size must be positive, got {max_retry_queue_size}"
            )

        # Create underlying OTLP exporter
        self._exporter = OTLPMetricExporter(endpoint=endpoint, headers=headers, timeout=timeout)

        # Queue and retry policy with optional persistence (Story 3-4)
        # Use signal-specific default path to prevent multi-exporter contention
        if persist_queue and persist_path is None:
            import os

            persist_path = os.path.expanduser("~/.mca_sdk/queue_metrics.json")

        self._queue = queue or TelemetryQueue(
            max_size=1000,
            persist=persist_queue,
            persist_path=persist_path,
            min_disk_space_mb=persist_min_disk_space_mb,
            config=config,
        )
        self._retry_policy = retry_policy or RetryPolicy(max_attempts=3)

        # Configuration
        self._drain_interval = drain_interval
        self._max_retry_queue_size = max_retry_queue_size
        self._max_retry_attempts = max_retry_attempts

        # Background worker state
        self._running = False
        self._worker_thread: Optional[threading.Thread] = None
        self._lock = threading.Lock()

        # Metrics for monitoring
        self._export_attempts = 0
        self._export_successes = 0
        self._export_failures = 0
        self._items_queued = 0
        self._items_requeued = 0
        self._items_dropped = 0

        # Persistence metrics (Story 3-4)
        self._meter = None
        self._persistence_writes_counter = None
        self._persistence_successes_counter = None
        self._persistence_failures_counter = None
        self._persistence_disk_full_counter = None
        self._persistence_corruptions_counter = None
        self._last_persistence_stats = {}

    def __getattr__(self, name: str):
        """Delegate attribute access to underlying exporter.

        This allows the SDK to access attributes like _preferred_temporality.
        """
        return getattr(self._exporter, name)

    def export(
        self, metrics_data: MetricsData, timeout_millis: float = 10000, **kwargs
    ) -> MetricExportResult:
        """Export metrics data (with automatic queueing on failure).

        This method is called by the OpenTelemetry SDK to export metrics.
        If export fails, data is automatically queued for retry.

        Args:
            metrics_data: Metrics to export
            timeout_millis: Timeout in milliseconds
            **kwargs: Additional keyword arguments

        Returns:
            MetricExportResult.SUCCESS if export succeeded or was queued

        Note:
            This method is called by the OTel SDK, not user code.
        """
        self._export_attempts += 1

        try:
            # Try to export directly first
            result = self._retry_policy.execute(
                self._exporter.export, metrics_data, timeout_millis=timeout_millis, **kwargs
            )

            if result == MetricExportResult.SUCCESS:
                self._export_successes += 1
                logger.debug("Metric export succeeded")
                return MetricExportResult.SUCCESS
            else:
                # Export failed, queue for retry
                self._export_failures += 1
                logger.warning(
                    f"Metric export failed: {result}, queueing for retry",
                    extra={"result": str(result)},
                )
                queued = self._queue_for_retry(metrics_data)
                # Return SUCCESS because data was accepted (queued), not FAILURE
                # Returning FAILURE would tell the caller the data was rejected/lost
                return MetricExportResult.SUCCESS if queued else MetricExportResult.FAILURE

        except CircuitBreakerError as e:
            # Circuit breaker is OPEN - fast-fail and queue immediately (Story 3-3)
            self._export_failures += 1
            logger.info(
                f"Circuit breaker open, queueing metrics for retry: {e}",
                extra={"circuit_state": "open"},
            )
            queued = self._queue_for_retry(metrics_data)
            # Return SUCCESS because data was accepted (queued), not FAILURE
            return MetricExportResult.SUCCESS if queued else MetricExportResult.FAILURE

        except Exception as e:
            # Exception during export, queue for retry
            self._export_failures += 1
            logger.error(f"Exception during metric export: {e}", extra={"error": str(e)})
            queued = self._queue_for_retry(metrics_data)
            # Return SUCCESS because data was accepted (queued), not FAILURE
            return MetricExportResult.SUCCESS if queued else MetricExportResult.FAILURE

    def _queue_for_retry(self, metrics_data: MetricsData) -> bool:
        """Queue failed metrics for retry (internal method).

        Args:
            metrics_data: Metrics to queue

        Returns:
            True if successfully queued, False if dropped
        """
        # Check if queue is getting too full
        if self._queue.size() >= self._max_retry_queue_size:
            logger.warning(
                "Queue size limit reached, dropping metrics to prevent unbounded growth",
                extra={
                    "queue_size": self._queue.size(),
                    "max_retry_queue_size": self._max_retry_queue_size,
                },
            )
            return False

        # Wrap in QueuedItem with attempt counter and enqueue
        queued_item = QueuedItem(data=metrics_data, attempt_count=0)
        queued = self._queue.enqueue(queued_item)
        if queued:
            self._items_queued += 1
            logger.debug(
                f"Queued metrics for retry (queue size: {self._queue.size()})",
                extra={"queue_size": self._queue.size()},
            )
            return True
        else:
            logger.warning("Queue is full, metrics were dropped")
            return False

    def start_background_worker(self) -> None:
        """Start background worker thread to drain queue.

        The worker periodically attempts to export queued metrics.
        Safe to call multiple times (no-op if already running).

        Example:
            >>> exporter.start_background_worker()
            >>> # Worker now drains queue every drain_interval seconds
        """
        with self._lock:
            if self._running:
                logger.debug("Background worker already running")
                return

            self._running = True
            self._worker_thread = threading.Thread(
                target=self._worker_loop, name="BufferedExporter-Worker", daemon=True
            )
            self._worker_thread.start()
            logger.info("Started background worker thread")

    def stop_background_worker(self) -> None:
        """Stop background worker thread.

        Waits for worker to finish current operation before stopping.
        Safe to call multiple times (no-op if not running).

        Example:
            >>> exporter.stop_background_worker()
        """
        with self._lock:
            if not self._running:
                return

            self._running = False

        # Wait for worker thread to finish
        if self._worker_thread and self._worker_thread.is_alive():
            self._worker_thread.join(timeout=5.0)
            logger.info("Stopped background worker thread")

    def set_meter(self, meter) -> None:
        """Set meter for persistence metrics instrumentation (Story 3-4).

        Args:
            meter: OpenTelemetry Meter instance for creating metrics

        Note:
            This method enables automatic emission of queue persistence metrics
            to the configured OTLP collector. Should be called after provider setup.
        """
        self._meter = meter

        # Create persistence metric counters
        self._persistence_writes_counter = meter.create_counter(
            "mca_sdk.queue.persistence.writes_total",
            description="Total persistence write attempts (overflow events)",
            unit="1",
        )
        self._persistence_successes_counter = meter.create_counter(
            "mca_sdk.queue.persistence.successes_total",
            description="Total successful persistence writes",
            unit="1",
        )
        self._persistence_failures_counter = meter.create_counter(
            "mca_sdk.queue.persistence.failures_total",
            description="Total failed persistence writes",
            unit="1",
        )
        self._persistence_disk_full_counter = meter.create_counter(
            "mca_sdk.queue.persistence.disk_full_total",
            description="Total persistence writes skipped due to low disk space",
            unit="1",
        )
        self._persistence_corruptions_counter = meter.create_counter(
            "mca_sdk.queue.persistence.corruptions_total",
            description="Total corrupted queue files detected on startup",
            unit="1",
        )

        logger.debug("Persistence metrics instrumentation enabled for metric exporter")

    def _emit_persistence_metrics(self) -> None:
        """Emit current persistence metrics to OTel collector (internal method).

        This method is called periodically by the background worker to emit
        the latest persistence statistics as OpenTelemetry metrics.
        """
        if not self._meter or not self._persistence_writes_counter:
            return

        stats = self._queue.buffer_stats()
        if "persistence" not in stats:
            return

        persistence_stats = stats["persistence"]

        # Emit deltas (only new events since last emit)
        for metric_key, counter in [
            ("writes_attempted", self._persistence_writes_counter),
            ("writes_succeeded", self._persistence_successes_counter),
            ("writes_failed", self._persistence_failures_counter),
            ("disk_full_skipped", self._persistence_disk_full_counter),
            ("corruptions_detected", self._persistence_corruptions_counter),
        ]:
            current_value = persistence_stats.get(metric_key, 0)
            last_value = self._last_persistence_stats.get(metric_key, 0)
            delta = current_value - last_value

            if delta > 0:
                counter.add(delta)
                self._last_persistence_stats[metric_key] = current_value

    def _export_with_retry(self, data):
        """Export metrics with retry policy."""
        return self._retry_policy.execute(self._exporter.export, data, timeout_millis=10000)

    def _is_success(self, result) -> bool:
        """Check if metric export was successful."""
        return result == MetricExportResult.SUCCESS

    def flush(self, timeout_millis: int = 30000) -> bool:
        """Flush all queued metrics.

        Attempts to export all queued metrics within the timeout period.
        Blocks until complete or timeout is reached.

        Args:
            timeout_millis: Maximum time to wait in milliseconds (default: 30000ms / 30s).
                          Note: This is intentionally longer than collector_timeout (10s) since
                          flush may need to export multiple batches sequentially.

        Returns:
            True if all metrics were flushed, False otherwise

        Example:
            >>> exporter.flush(timeout_millis=10000)  # Wait up to 10s
            True
        """
        timeout_seconds = timeout_millis / 1000.0
        start_time = time.time()

        logger.info(
            f"Flushing queue ({self._queue.size()} items)",
            extra={"queue_size": self._queue.size(), "timeout_seconds": timeout_seconds},
        )

        while self._queue.size() > 0:
            # Check timeout
            elapsed = time.time() - start_time
            if elapsed >= timeout_seconds:
                logger.warning(
                    f"Flush timeout reached, {self._queue.size()} items remain",
                    extra={"queue_size": self._queue.size()},
                )
                return False

            # Drain one item
            item = self._queue.dequeue()
            if item is None:
                break  # Queue empty

            # Unwrap QueuedItem (with backward compatibility)
            if isinstance(item, QueuedItem):
                metrics_data = item.data
            else:
                metrics_data = item

            try:
                result = self._retry_policy.execute(
                    self._exporter.export, metrics_data, timeout_millis=10000
                )
                if result != MetricExportResult.SUCCESS:
                    logger.warning(f"Failed to flush metrics: {result}")
            except Exception as e:
                logger.error(f"Exception during flush: {e}")

        # Also flush underlying exporter
        try:
            self._exporter.force_flush(timeout_millis=int(timeout_millis))
        except Exception as e:
            logger.error(f"Error flushing underlying exporter: {e}")

        logger.info("Flush completed")
        return self._queue.size() == 0

    def force_flush(self, timeout_millis: int = 30000) -> bool:
        """Force flush all queued telemetry (OTel SDK compatibility).

        This method provides OpenTelemetry SDK compatibility by wrapping flush
        with exception handling. It never raises exceptions.

        Args:
            timeout_millis: Maximum time to wait in milliseconds

        Returns:
            True if flush succeeded, False otherwise (including exceptions)

        Example:
            >>> if exporter.force_flush(timeout_millis=10000):
            ...     print("Flush succeeded")
        """
        try:
            return self.flush(timeout_millis=timeout_millis)
        except Exception as e:
            logger.critical(f"force_flush failed with unexpected exception: {e}", exc_info=True)
            return False

    def shutdown(self, timeout_millis: float = 30000, **kwargs) -> None:
        """Shutdown exporter gracefully.

        Stops background worker, flushes remaining metrics, and cleans up resources.

        Args:
            timeout_millis: Timeout in milliseconds
            **kwargs: Additional keyword arguments

        Example:
            >>> exporter.shutdown()
        """
        logger.info("Shutting down buffered exporter")

        # Emit final persistence metrics before shutdown
        self._emit_persistence_metrics()

        # Stop background worker
        self.stop_background_worker()

        # Final flush (use half the timeout for flush)
        self.flush(timeout_millis=timeout_millis / 2)

        # Shutdown underlying exporter
        try:
            self._exporter.shutdown(timeout_millis=timeout_millis / 2, **kwargs)
        except Exception as e:
            logger.error(f"Error shutting down underlying exporter: {e}")

        logger.info(
            f"Buffered exporter shutdown complete. Stats: "
            f"attempts={self._export_attempts}, "
            f"successes={self._export_successes}, "
            f"failures={self._export_failures}, "
            f"queued={self._items_queued}, "
            f"requeued={self._items_requeued}"
        )

    def get_stats(self) -> dict:
        """Get exporter statistics.

        Returns:
            Dictionary with export statistics

        Example:
            >>> stats = exporter.get_stats()
            >>> print(f"Success rate: {stats['successes'] / stats['attempts']:.2%}")
        """
        return {
            "export_attempts": self._export_attempts,
            "export_successes": self._export_successes,
            "export_failures": self._export_failures,
            "items_queued": self._items_queued,
            "items_requeued": self._items_requeued,
            "items_dropped": self._items_dropped,
            "queue_size": self._queue.size(),
        }


class BufferedOTLPLogExporter(BufferedOTLPExporterBase):
    """Buffered wrapper for OTLP log exporter.

    Wraps the standard OTLPLogExporter to provide automatic queueing
    and retry logic when the collector is temporarily unreachable.

    Features:
        - Automatic queueing of failed exports
        - Background worker thread drains queue periodically
        - Retry logic with exponential backoff
        - Graceful shutdown with final flush
        - Prevents infinite queue growth on persistent failures

    Thread Safety:
        All operations are thread-safe. The background worker runs in
        a separate daemon thread.

    Examples:
        >>> queue = TelemetryQueue(max_size=1000)
        >>> retry_policy = RetryPolicy(max_attempts=3)
        >>> exporter = BufferedOTLPLogExporter(
        ...     endpoint="http://localhost:4318/v1/logs",
        ...     queue=queue,
        ...     retry_policy=retry_policy
        ... )
        >>> # Exporter will automatically queue and retry failed exports
    """

    def __init__(
        self,
        endpoint: str = "http://localhost:4318/v1/logs",
        headers: Optional[dict] = None,
        timeout: int = 10,
        queue: Optional[TelemetryQueue] = None,
        retry_policy: Optional[RetryPolicy] = None,
        drain_interval: float = 1.0,
        max_retry_queue_size: int = 100,
        max_retry_attempts: int = 3,
        persist_queue: bool = False,
        persist_path: Optional[str] = None,
        persist_min_disk_space_mb: float = 10.0,
        config: Optional[Any] = None,
    ):
        """Initialize buffered log exporter.

        Args:
            endpoint: OTLP collector endpoint URL
            headers: Optional HTTP headers for requests
            timeout: Request timeout in seconds
            queue: TelemetryQueue for buffering (creates default if None)
            retry_policy: RetryPolicy for retries (creates default if None)
            drain_interval: Seconds between queue drain attempts (default: 1.0)
            max_retry_queue_size: Max items to re-queue on failure (default: 100)
            max_retry_attempts: Max dequeue attempts before dropping item (default: 3)
            persist_queue: Whether to persist queue to disk (default: False)
            persist_path: Path for queue persistence (required if persist_queue=True)
            persist_min_disk_space_mb: Minimum free disk space (MB) for persistence (default: 10.0)
            config: Optional MCAConfig for custom allowed directories (Story 3.15)

        Raises:
            BufferingError: If configuration is invalid
        """
        if drain_interval <= 0:
            raise BufferingError(f"drain_interval must be positive, got {drain_interval}")

        if max_retry_queue_size <= 0:
            raise BufferingError(
                f"max_retry_queue_size must be positive, got {max_retry_queue_size}"
            )

        # Create underlying OTLP exporter
        self._exporter = OTLPLogExporter(endpoint=endpoint, headers=headers, timeout=timeout)

        # Queue and retry policy with optional persistence (Story 3-4)
        # Use signal-specific default path to prevent multi-exporter contention
        if persist_queue and persist_path is None:
            import os

            persist_path = os.path.expanduser("~/.mca_sdk/queue_logs.json")

        self._queue = queue or TelemetryQueue(
            max_size=1000,
            persist=persist_queue,
            persist_path=persist_path,
            min_disk_space_mb=persist_min_disk_space_mb,
            config=config,
        )
        self._retry_policy = retry_policy or RetryPolicy(max_attempts=3)

        # Configuration
        self._drain_interval = drain_interval
        self._max_retry_queue_size = max_retry_queue_size
        self._max_retry_attempts = max_retry_attempts

        # Background worker state
        self._running = False
        self._worker_thread: Optional[threading.Thread] = None
        self._lock = threading.Lock()

        # Metrics for monitoring
        self._export_attempts = 0
        self._export_successes = 0
        self._export_failures = 0
        self._items_queued = 0
        self._items_requeued = 0
        self._items_dropped = 0

        # Persistence metrics (Story 3-4)
        self._meter = None
        self._persistence_writes_counter = None
        self._persistence_successes_counter = None
        self._persistence_failures_counter = None
        self._persistence_disk_full_counter = None
        self._persistence_corruptions_counter = None
        self._last_persistence_stats = {}

    def set_meter(self, meter) -> None:
        """Set meter for persistence metrics instrumentation (Story 3-4).

        Args:
            meter: OpenTelemetry Meter instance for creating metrics

        Note:
            This method enables automatic emission of queue persistence metrics
            to the configured OTLP collector. Should be called after provider setup.
        """
        self._meter = meter

        # Create persistence metric counters
        self._persistence_writes_counter = meter.create_counter(
            "mca_sdk.queue.persistence.writes_total",
            description="Total persistence write attempts (overflow events)",
            unit="1",
        )
        self._persistence_successes_counter = meter.create_counter(
            "mca_sdk.queue.persistence.successes_total",
            description="Total successful persistence writes",
            unit="1",
        )
        self._persistence_failures_counter = meter.create_counter(
            "mca_sdk.queue.persistence.failures_total",
            description="Total failed persistence writes",
            unit="1",
        )
        self._persistence_disk_full_counter = meter.create_counter(
            "mca_sdk.queue.persistence.disk_full_total",
            description="Total persistence writes skipped due to low disk space",
            unit="1",
        )
        self._persistence_corruptions_counter = meter.create_counter(
            "mca_sdk.queue.persistence.corruptions_total",
            description="Total corrupted queue files detected on startup",
            unit="1",
        )

        logger.debug("Persistence metrics instrumentation enabled for log exporter")

    def _emit_persistence_metrics(self) -> None:
        """Emit current persistence metrics to OTel collector (internal method).

        This method is called periodically by the background worker to emit
        the latest persistence statistics as OpenTelemetry metrics.
        """
        if not self._meter or not self._persistence_writes_counter:
            return

        stats = self._queue.buffer_stats()
        if "persistence" not in stats:
            return

        persistence_stats = stats["persistence"]

        # Emit deltas (only new events since last emit)
        for metric_key, counter in [
            ("writes_attempted", self._persistence_writes_counter),
            ("writes_succeeded", self._persistence_successes_counter),
            ("writes_failed", self._persistence_failures_counter),
            ("disk_full_skipped", self._persistence_disk_full_counter),
            ("corruptions_detected", self._persistence_corruptions_counter),
        ]:
            current_value = persistence_stats.get(metric_key, 0)
            last_value = self._last_persistence_stats.get(metric_key, 0)
            delta = current_value - last_value

            if delta > 0:
                counter.add(delta)
                self._last_persistence_stats[metric_key] = current_value

    def __getattr__(self, name: str):
        """Delegate attribute access to underlying exporter.

        This allows the SDK to access necessary exporter attributes.
        """
        return getattr(self._exporter, name)

    def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult:
        """Export log data (with automatic queueing on failure).

        This method is called by the OpenTelemetry SDK to export logs.
        If export fails, data is automatically queued for retry.

        Args:
            batch: Logs to export

        Returns:
            LogRecordExportResult.SUCCESS if export succeeded or was queued

        Note:
            This method is called by the OTel SDK, not user code.
        """
        self._export_attempts += 1

        try:
            # Try to export directly first
            result = self._retry_policy.execute(self._exporter.export, batch)

            if result == LogRecordExportResult.SUCCESS:
                self._export_successes += 1
                logger.debug("Log export succeeded")
                return LogRecordExportResult.SUCCESS
            else:
                # Export failed, queue for retry
                self._export_failures += 1
                logger.warning(
                    f"Log export failed: {result}, queueing for retry",
                    extra={"result": str(result)},
                )
                queued = self._queue_for_retry(batch)
                # Return SUCCESS because data was accepted (queued), not FAILURE
                return LogRecordExportResult.SUCCESS if queued else LogRecordExportResult.FAILURE

        except CircuitBreakerError as e:
            # Circuit breaker is OPEN - fast-fail and queue immediately (Story 3-3)
            self._export_failures += 1
            logger.info(
                f"Circuit breaker open, queueing logs for retry: {e}",
                extra={"circuit_state": "open"},
            )
            queued = self._queue_for_retry(batch)
            # Return SUCCESS because data was accepted (queued), not FAILURE
            return LogRecordExportResult.SUCCESS if queued else LogRecordExportResult.FAILURE

        except Exception as e:
            # Exception during export, queue for retry
            self._export_failures += 1
            logger.error(f"Exception during log export: {e}", extra={"error": str(e)})
            queued = self._queue_for_retry(batch)
            # Return SUCCESS because data was accepted (queued), not FAILURE
            return LogRecordExportResult.SUCCESS if queued else LogRecordExportResult.FAILURE

    def _queue_for_retry(self, batch: Sequence[ReadableLogRecord]) -> bool:
        """Queue failed logs for retry (internal method).

        Args:
            batch: Logs to queue

        Returns:
            True if successfully queued, False if dropped
        """
        # Check if queue is getting too full
        if self._queue.size() >= self._max_retry_queue_size:
            logger.warning(
                "Queue size limit reached, dropping logs to prevent unbounded growth",
                extra={
                    "queue_size": self._queue.size(),
                    "max_retry_queue_size": self._max_retry_queue_size,
                },
            )
            return False

        # Wrap in QueuedItem with attempt counter and enqueue
        queued_item = QueuedItem(data=batch, attempt_count=0)
        queued = self._queue.enqueue(queued_item)
        if queued:
            self._items_queued += 1
            logger.debug(
                f"Queued logs for retry (queue size: {self._queue.size()})",
                extra={"queue_size": self._queue.size()},
            )
            return True
        else:
            logger.warning("Queue is full, logs were dropped")
            return False

    def start_background_worker(self) -> None:
        """Start background worker thread to drain queue.

        The worker periodically attempts to export queued logs.
        Safe to call multiple times (no-op if already running).

        Example:
            >>> exporter.start_background_worker()
            >>> # Worker now drains queue every drain_interval seconds
        """
        with self._lock:
            if self._running:
                logger.debug("Background worker already running")
                return

            self._running = True
            self._worker_thread = threading.Thread(
                target=self._worker_loop, name="BufferedLogExporter-Worker", daemon=True
            )
            self._worker_thread.start()
            logger.info("Started background worker thread")

    def stop_background_worker(self) -> None:
        """Stop background worker thread.

        Waits for worker to finish current operation before stopping.
        Safe to call multiple times (no-op if not running).

        Example:
            >>> exporter.stop_background_worker()
        """
        with self._lock:
            if not self._running:
                return

            self._running = False

        # Wait for worker thread to finish
        if self._worker_thread and self._worker_thread.is_alive():
            self._worker_thread.join(timeout=5.0)
            logger.info("Stopped background worker thread")

    def _export_with_retry(self, data):
        """Export logs with retry policy."""
        return self._retry_policy.execute(self._exporter.export, data)

    def _is_success(self, result) -> bool:
        """Check if log export was successful."""
        return result == LogRecordExportResult.SUCCESS

    def flush(self, timeout_millis: int = 30000) -> bool:
        """Flush all queued logs.

        Attempts to export all queued logs within the timeout period.
        Blocks until complete or timeout is reached.

        Args:
            timeout_millis: Maximum time to wait in milliseconds

        Returns:
            True if all logs were flushed, False otherwise

        Example:
            >>> exporter.flush(timeout_millis=10000)  # Wait up to 10s
            True
        """
        timeout_seconds = timeout_millis / 1000.0
        start_time = time.time()

        logger.info(
            f"Flushing queue ({self._queue.size()} items)",
            extra={"queue_size": self._queue.size(), "timeout_seconds": timeout_seconds},
        )

        while self._queue.size() > 0:
            # Check timeout
            elapsed = time.time() - start_time
            if elapsed >= timeout_seconds:
                logger.warning(
                    f"Flush timeout reached, {self._queue.size()} items remain",
                    extra={"queue_size": self._queue.size()},
                )
                return False

            # Drain one item
            item = self._queue.dequeue()
            if item is None:
                break  # Queue empty

            # Unwrap QueuedItem (with backward compatibility)
            if isinstance(item, QueuedItem):
                log_batch = item.data
            else:
                log_batch = item

            try:
                result = self._retry_policy.execute(self._exporter.export, log_batch)
                if result != LogRecordExportResult.SUCCESS:
                    logger.warning(f"Failed to flush logs: {result}")
            except Exception as e:
                logger.error(f"Exception during flush: {e}")

        # Also flush underlying exporter
        try:
            self._exporter.force_flush(timeout_millis=int(timeout_millis))
        except Exception as e:
            logger.error(f"Error flushing underlying exporter: {e}")

        logger.info("Flush completed")
        return self._queue.size() == 0

    def force_flush(self, timeout_millis: int = 30000) -> bool:
        """Force flush all queued telemetry (OTel SDK compatibility).

        This method provides OpenTelemetry SDK compatibility by wrapping flush
        with exception handling. It never raises exceptions.

        Args:
            timeout_millis: Maximum time to wait in milliseconds

        Returns:
            True if flush succeeded, False otherwise (including exceptions)

        Example:
            >>> if exporter.force_flush(timeout_millis=10000):
            ...     print("Flush succeeded")
        """
        try:
            return self.flush(timeout_millis=timeout_millis)
        except Exception as e:
            logger.critical(f"force_flush failed with unexpected exception: {e}", exc_info=True)
            return False

    def shutdown(self, timeout_millis: float = 30000, **kwargs) -> None:
        """Shutdown exporter gracefully.

        Stops background worker, flushes remaining logs, and cleans up resources.

        Args:
            timeout_millis: Timeout in milliseconds
            **kwargs: Additional keyword arguments

        Example:
            >>> exporter.shutdown()
        """
        logger.info("Shutting down buffered log exporter")

        # Emit final persistence metrics before shutdown
        self._emit_persistence_metrics()

        # Stop background worker
        self.stop_background_worker()

        # Final flush (use half the timeout for flush)
        self.flush(timeout_millis=timeout_millis / 2)

        # Shutdown underlying exporter
        try:
            self._exporter.shutdown()
        except Exception as e:
            logger.error(f"Error shutting down underlying exporter: {e}")

        logger.info(
            f"Buffered log exporter shutdown complete. Stats: "
            f"attempts={self._export_attempts}, "
            f"successes={self._export_successes}, "
            f"failures={self._export_failures}, "
            f"queued={self._items_queued}, "
            f"requeued={self._items_requeued}"
        )

    def get_stats(self) -> dict:
        """Get exporter statistics.

        Returns:
            Dictionary with export statistics

        Example:
            >>> stats = exporter.get_stats()
            >>> print(f"Success rate: {stats['successes'] / stats['attempts']:.2%}")
        """
        return {
            "export_attempts": self._export_attempts,
            "export_successes": self._export_successes,
            "export_failures": self._export_failures,
            "items_queued": self._items_queued,
            "items_requeued": self._items_requeued,
            "items_dropped": self._items_dropped,
            "queue_size": self._queue.size(),
        }


class BufferedOTLPSpanExporter(BufferedOTLPExporterBase):
    """Buffered wrapper for OTLP span exporter.

    Wraps the standard OTLPSpanExporter to provide automatic queueing
    and retry logic when the collector is temporarily unreachable.

    Features:
        - Automatic queueing of failed exports
        - Background worker thread drains queue periodically
        - Retry logic with exponential backoff
        - Graceful shutdown with final flush
        - Prevents infinite queue growth on persistent failures

    Thread Safety:
        All operations are thread-safe. The background worker runs in
        a separate daemon thread.

    Examples:
        >>> queue = TelemetryQueue(max_size=1000)
        >>> retry_policy = RetryPolicy(max_attempts=3)
        >>> exporter = BufferedOTLPSpanExporter(
        ...     endpoint="http://localhost:4318/v1/traces",
        ...     queue=queue,
        ...     retry_policy=retry_policy
        ... )
        >>> # Exporter will automatically queue and retry failed exports
    """

    def __init__(
        self,
        endpoint: str = "http://localhost:4318/v1/traces",
        headers: Optional[dict] = None,
        timeout: int = 10,
        queue: Optional[TelemetryQueue] = None,
        retry_policy: Optional[RetryPolicy] = None,
        drain_interval: float = 1.0,
        max_retry_queue_size: int = 100,
        max_retry_attempts: int = 3,
        persist_queue: bool = False,
        persist_path: Optional[str] = None,
        persist_min_disk_space_mb: float = 10.0,
        config: Optional[Any] = None,
    ):
        """Initialize buffered span exporter.

        Args:
            endpoint: OTLP collector endpoint URL
            headers: Optional HTTP headers for requests
            timeout: Request timeout in seconds
            queue: TelemetryQueue for buffering (creates default if None)
            retry_policy: RetryPolicy for retries (creates default if None)
            drain_interval: Seconds between queue drain attempts (default: 1.0)
            max_retry_queue_size: Max items to re-queue on failure (default: 100)
            max_retry_attempts: Max dequeue attempts before dropping item (default: 3)
            persist_queue: Whether to persist queue to disk (default: False)
            persist_path: Path for queue persistence (required if persist_queue=True)
            persist_min_disk_space_mb: Minimum free disk space (MB) for persistence (default: 10.0)
            config: Optional MCAConfig for custom allowed directories (Story 3.15)

        Raises:
            BufferingError: If configuration is invalid
        """
        if drain_interval <= 0:
            raise BufferingError(f"drain_interval must be positive, got {drain_interval}")

        if max_retry_queue_size <= 0:
            raise BufferingError(
                f"max_retry_queue_size must be positive, got {max_retry_queue_size}"
            )

        # Create underlying OTLP exporter
        self._exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers, timeout=timeout)

        # Queue and retry policy with optional persistence (Story 3-4)
        # Use signal-specific default path to prevent multi-exporter contention
        if persist_queue and persist_path is None:
            import os

            persist_path = os.path.expanduser("~/.mca_sdk/queue_traces.json")

        self._queue = queue or TelemetryQueue(
            max_size=1000,
            persist=persist_queue,
            persist_path=persist_path,
            min_disk_space_mb=persist_min_disk_space_mb,
            config=config,
        )
        self._retry_policy = retry_policy or RetryPolicy(max_attempts=3)

        # Configuration
        self._drain_interval = drain_interval
        self._max_retry_queue_size = max_retry_queue_size
        self._max_retry_attempts = max_retry_attempts

        # Background worker state
        self._running = False
        self._worker_thread: Optional[threading.Thread] = None
        self._lock = threading.Lock()

        # Metrics for monitoring
        self._export_attempts = 0
        self._export_successes = 0
        self._export_failures = 0
        self._items_queued = 0
        self._items_requeued = 0
        self._items_dropped = 0

        # Persistence metrics (Story 3-4)
        self._meter = None
        self._persistence_writes_counter = None
        self._persistence_successes_counter = None
        self._persistence_failures_counter = None
        self._persistence_disk_full_counter = None
        self._persistence_corruptions_counter = None
        self._last_persistence_stats = {}

    def set_meter(self, meter) -> None:
        """Set meter for persistence metrics instrumentation (Story 3-4).

        Args:
            meter: OpenTelemetry Meter instance for creating metrics

        Note:
            This method enables automatic emission of queue persistence metrics
            to the configured OTLP collector. Should be called after provider setup.
        """
        self._meter = meter

        # Create persistence metric counters
        self._persistence_writes_counter = meter.create_counter(
            "mca_sdk.queue.persistence.writes_total",
            description="Total persistence write attempts (overflow events)",
            unit="1",
        )
        self._persistence_successes_counter = meter.create_counter(
            "mca_sdk.queue.persistence.successes_total",
            description="Total successful persistence writes",
            unit="1",
        )
        self._persistence_failures_counter = meter.create_counter(
            "mca_sdk.queue.persistence.failures_total",
            description="Total failed persistence writes",
            unit="1",
        )
        self._persistence_disk_full_counter = meter.create_counter(
            "mca_sdk.queue.persistence.disk_full_total",
            description="Total persistence writes skipped due to low disk space",
            unit="1",
        )
        self._persistence_corruptions_counter = meter.create_counter(
            "mca_sdk.queue.persistence.corruptions_total",
            description="Total corrupted queue files detected on startup",
            unit="1",
        )

        logger.debug("Persistence metrics instrumentation enabled for span exporter")

    def _emit_persistence_metrics(self) -> None:
        """Emit current persistence metrics to OTel collector (internal method).

        This method is called periodically by the background worker to emit
        the latest persistence statistics as OpenTelemetry metrics.
        """
        if not self._meter or not self._persistence_writes_counter:
            return

        stats = self._queue.buffer_stats()
        if "persistence" not in stats:
            return

        persistence_stats = stats["persistence"]

        # Emit deltas (only new events since last emit)
        for metric_key, counter in [
            ("writes_attempted", self._persistence_writes_counter),
            ("writes_succeeded", self._persistence_successes_counter),
            ("writes_failed", self._persistence_failures_counter),
            ("disk_full_skipped", self._persistence_disk_full_counter),
            ("corruptions_detected", self._persistence_corruptions_counter),
        ]:
            current_value = persistence_stats.get(metric_key, 0)
            last_value = self._last_persistence_stats.get(metric_key, 0)
            delta = current_value - last_value

            if delta > 0:
                counter.add(delta)
                self._last_persistence_stats[metric_key] = current_value

    def __getattr__(self, name: str):
        """Delegate attribute access to underlying exporter.

        This allows the SDK to access necessary exporter attributes.
        """
        return getattr(self._exporter, name)

    def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
        """Export span data (with automatic queueing on failure).

        This method is called by the OpenTelemetry SDK to export spans.
        If export fails, data is automatically queued for retry.

        Args:
            spans: Spans to export

        Returns:
            SpanExportResult.SUCCESS if export succeeded or was queued

        Note:
            This method is called by the OTel SDK, not user code.
        """
        self._export_attempts += 1

        try:
            # Try to export directly first
            result = self._retry_policy.execute(self._exporter.export, spans)

            if result == SpanExportResult.SUCCESS:
                self._export_successes += 1
                logger.debug("Span export succeeded")
                return SpanExportResult.SUCCESS
            else:
                # Export failed, queue for retry
                self._export_failures += 1
                logger.warning(
                    f"Span export failed: {result}, queueing for retry",
                    extra={"result": str(result)},
                )
                queued = self._queue_for_retry(spans)
                # Return SUCCESS because data was accepted (queued), not FAILURE
                return SpanExportResult.SUCCESS if queued else SpanExportResult.FAILURE

        except CircuitBreakerError as e:
            # Circuit breaker is OPEN - fast-fail and queue immediately (Story 3-3)
            self._export_failures += 1
            logger.info(
                f"Circuit breaker open, queueing spans for retry: {e}",
                extra={"circuit_state": "open"},
            )
            queued = self._queue_for_retry(spans)
            # Return SUCCESS because data was accepted (queued), not FAILURE
            return SpanExportResult.SUCCESS if queued else SpanExportResult.FAILURE

        except Exception as e:
            # Exception during export, queue for retry
            self._export_failures += 1
            logger.error(f"Exception during span export: {e}", extra={"error": str(e)})
            queued = self._queue_for_retry(spans)
            # Return SUCCESS because data was accepted (queued), not FAILURE
            return SpanExportResult.SUCCESS if queued else SpanExportResult.FAILURE

    def _queue_for_retry(self, spans: Sequence[ReadableSpan]) -> bool:
        """Queue failed spans for retry (internal method).

        Args:
            spans: Spans to queue

        Returns:
            True if successfully queued, False if dropped
        """
        # Check if queue is getting too full
        if self._queue.size() >= self._max_retry_queue_size:
            logger.warning(
                "Queue size limit reached, dropping spans to prevent unbounded growth",
                extra={
                    "queue_size": self._queue.size(),
                    "max_retry_queue_size": self._max_retry_queue_size,
                },
            )
            return False

        # Wrap in QueuedItem with attempt counter and enqueue
        queued_item = QueuedItem(data=spans, attempt_count=0)
        queued = self._queue.enqueue(queued_item)
        if queued:
            self._items_queued += 1
            logger.debug(
                f"Queued spans for retry (queue size: {self._queue.size()})",
                extra={"queue_size": self._queue.size()},
            )
            return True
        else:
            logger.warning("Queue is full, spans were dropped")
            return False

    def start_background_worker(self) -> None:
        """Start background worker thread to drain queue.

        The worker periodically attempts to export queued spans.
        Safe to call multiple times (no-op if already running).

        Example:
            >>> exporter.start_background_worker()
            >>> # Worker now drains queue every drain_interval seconds
        """
        with self._lock:
            if self._running:
                logger.debug("Background worker already running")
                return

            self._running = True
            self._worker_thread = threading.Thread(
                target=self._worker_loop, name="BufferedSpanExporter-Worker", daemon=True
            )
            self._worker_thread.start()
            logger.info("Started background worker thread")

    def stop_background_worker(self) -> None:
        """Stop background worker thread.

        Waits for worker to finish current operation before stopping.
        Safe to call multiple times (no-op if not running).

        Example:
            >>> exporter.stop_background_worker()
        """
        with self._lock:
            if not self._running:
                return

            self._running = False

        # Wait for worker thread to finish
        if self._worker_thread and self._worker_thread.is_alive():
            self._worker_thread.join(timeout=5.0)
            logger.info("Stopped background worker thread")

    def _export_with_retry(self, data):
        """Export spans with retry policy."""
        return self._retry_policy.execute(self._exporter.export, data)

    def _is_success(self, result) -> bool:
        """Check if span export was successful."""
        return result == SpanExportResult.SUCCESS

    def flush(self, timeout_millis: int = 30000) -> bool:
        """Flush all queued spans.

        Attempts to export all queued spans within the timeout period.
        Blocks until complete or timeout is reached.

        Args:
            timeout_millis: Maximum time to wait in milliseconds

        Returns:
            True if all spans were flushed, False otherwise

        Example:
            >>> exporter.flush(timeout_millis=10000)  # Wait up to 10s
            True
        """
        timeout_seconds = timeout_millis / 1000.0
        start_time = time.time()

        logger.info(
            f"Flushing queue ({self._queue.size()} items)",
            extra={"queue_size": self._queue.size(), "timeout_seconds": timeout_seconds},
        )

        while self._queue.size() > 0:
            # Check timeout
            elapsed = time.time() - start_time
            if elapsed >= timeout_seconds:
                logger.warning(
                    f"Flush timeout reached, {self._queue.size()} items remain",
                    extra={"queue_size": self._queue.size()},
                )
                return False

            # Drain one item
            item = self._queue.dequeue()
            if item is None:
                break  # Queue empty

            # Unwrap QueuedItem (with backward compatibility)
            if isinstance(item, QueuedItem):
                span_batch = item.data
            else:
                span_batch = item

            try:
                result = self._retry_policy.execute(self._exporter.export, span_batch)
                if result != SpanExportResult.SUCCESS:
                    logger.warning(f"Failed to flush spans: {result}")
            except Exception as e:
                logger.error(f"Exception during flush: {e}")

        # Also flush underlying exporter
        try:
            self._exporter.force_flush(timeout_millis=int(timeout_millis))
        except Exception as e:
            logger.error(f"Error flushing underlying exporter: {e}")

        logger.info("Flush completed")
        return self._queue.size() == 0

    def force_flush(self, timeout_millis: int = 30000) -> bool:
        """Force flush all queued telemetry (OTel SDK compatibility).

        This method provides OpenTelemetry SDK compatibility by wrapping flush
        with exception handling. It never raises exceptions.

        Args:
            timeout_millis: Maximum time to wait in milliseconds

        Returns:
            True if flush succeeded, False otherwise (including exceptions)

        Example:
            >>> if exporter.force_flush(timeout_millis=10000):
            ...     print("Flush succeeded")
        """
        try:
            return self.flush(timeout_millis=timeout_millis)
        except Exception as e:
            logger.critical(f"force_flush failed with unexpected exception: {e}", exc_info=True)
            return False

    def shutdown(self, timeout_millis: float = 30000, **kwargs) -> None:
        """Shutdown exporter gracefully.

        Stops background worker, flushes remaining spans, and cleans up resources.

        Args:
            timeout_millis: Timeout in milliseconds
            **kwargs: Additional keyword arguments

        Example:
            >>> exporter.shutdown()
        """
        logger.info("Shutting down buffered span exporter")

        # Emit final persistence metrics before shutdown
        self._emit_persistence_metrics()

        # Stop background worker
        self.stop_background_worker()

        # Final flush (use half the timeout for flush)
        self.flush(timeout_millis=timeout_millis / 2)

        # Shutdown underlying exporter
        try:
            self._exporter.shutdown()
        except Exception as e:
            logger.error(f"Error shutting down underlying exporter: {e}")

        logger.info(
            f"Buffered span exporter shutdown complete. Stats: "
            f"attempts={self._export_attempts}, "
            f"successes={self._export_successes}, "
            f"failures={self._export_failures}, "
            f"queued={self._items_queued}, "
            f"requeued={self._items_requeued}"
        )

    def get_stats(self) -> dict:
        """Get exporter statistics.

        Returns:
            Dictionary with export statistics

        Example:
            >>> stats = exporter.get_stats()
            >>> print(f"Success rate: {stats['successes'] / stats['attempts']:.2%}")
        """
        return {
            "export_attempts": self._export_attempts,
            "export_successes": self._export_successes,
            "export_failures": self._export_failures,
            "items_queued": self._items_queued,
            "items_requeued": self._items_requeued,
            "items_dropped": self._items_dropped,
            "queue_size": self._queue.size(),
        }
