# -*- coding: utf-8 -*-
"""Location: ./mcpgateway/services/metrics_buffer_service.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Buffered metrics service for batching metric writes to the database.
This service accumulates metrics in memory and flushes them to the database
periodically, reducing DB write pressure under high load.
"""

# Standard
import asyncio
from collections import deque
from dataclasses import dataclass
from datetime import datetime, timezone
import logging
import threading
import time
from typing import Deque, Optional

# First-Party
from mcpgateway.config import settings
from mcpgateway.db import A2AAgentMetric, fresh_db_session, PromptMetric, ResourceMetric, ServerMetric, ToolMetric

logger = logging.getLogger(__name__)


@dataclass
class BufferedToolMetric:
    """Buffered tool metric entry."""

    tool_id: str
    timestamp: datetime
    response_time: float
    is_success: bool
    error_message: Optional[str] = None


@dataclass
class BufferedResourceMetric:
    """Buffered resource metric entry."""

    resource_id: str
    timestamp: datetime
    response_time: float
    is_success: bool
    error_message: Optional[str] = None


@dataclass
class BufferedPromptMetric:
    """Buffered prompt metric entry."""

    prompt_id: str
    timestamp: datetime
    response_time: float
    is_success: bool
    error_message: Optional[str] = None


@dataclass
class BufferedServerMetric:
    """Buffered server metric entry."""

    server_id: str
    timestamp: datetime
    response_time: float
    is_success: bool
    error_message: Optional[str] = None


@dataclass
class BufferedA2AAgentMetric:
    """Buffered A2A agent metric entry."""

    a2a_agent_id: str
    timestamp: datetime
    response_time: float
    is_success: bool
    interaction_type: str = "invoke"
    error_message: Optional[str] = None


class MetricsBufferService:
    """Service for buffering and batching metrics writes to the database.

    This service provides:
    - Thread-safe buffering of tool, resource, prompt, server, and A2A agent metrics
    - Periodic flushing to database (configurable interval)
    - Graceful shutdown with final flush

    Configuration (via environment variables):
    - METRICS_BUFFER_ENABLED: Enable buffered metrics (default: True)
    - METRICS_BUFFER_FLUSH_INTERVAL: Seconds between flushes (default: 60)
    - METRICS_BUFFER_MAX_SIZE: Max entries before forced flush (default: 1000)
    """

    def __init__(
        self,
        flush_interval: Optional[int] = None,
        max_buffer_size: Optional[int] = None,
        enabled: Optional[bool] = None,
    ):
        """Initialize the metrics buffer service.

        Args:
            flush_interval: Seconds between automatic flushes (default: from settings or 60)
            max_buffer_size: Maximum buffer entries before forced flush (default: from settings or 1000)
            enabled: Whether buffering is enabled (default: from settings or True)
        """
        self.flush_interval = flush_interval or getattr(settings, "metrics_buffer_flush_interval", 60)
        self.max_buffer_size = max_buffer_size or getattr(settings, "metrics_buffer_max_size", 1000)
        self.enabled = enabled if enabled is not None else getattr(settings, "metrics_buffer_enabled", True)
        self.recording_enabled = getattr(settings, "db_metrics_recording_enabled", True)

        # Thread-safe buffers using deque with locks
        self._tool_metrics: Deque[BufferedToolMetric] = deque()
        self._resource_metrics: Deque[BufferedResourceMetric] = deque()
        self._prompt_metrics: Deque[BufferedPromptMetric] = deque()
        self._server_metrics: Deque[BufferedServerMetric] = deque()
        self._a2a_agent_metrics: Deque[BufferedA2AAgentMetric] = deque()
        self._lock = threading.Lock()

        # Background flush task
        self._flush_task: Optional[asyncio.Task] = None
        self._shutdown_event = asyncio.Event()

        # Stats for monitoring
        self._total_buffered = 0
        self._total_flushed = 0
        self._flush_count = 0

        logger.info(
            f"MetricsBufferService initialized: recording_enabled={self.recording_enabled}, "
            f"buffer_enabled={self.enabled}, flush_interval={self.flush_interval}s, max_buffer_size={self.max_buffer_size}"
        )

    async def start(self) -> None:
        """Start the background flush task."""
        if not self.recording_enabled:
            logger.info("MetricsBufferService: recording disabled, skipping flush loop")
            return
        if not self.enabled:
            logger.info("MetricsBufferService disabled, skipping start")
            return

        current_loop = asyncio.get_running_loop()
        if not self._flush_task_is_active_for_loop(current_loop):
            self._shutdown_event.clear()
            self._flush_task = asyncio.create_task(self._flush_loop())
            logger.info("MetricsBufferService flush task started")

    def _flush_task_is_active_for_loop(self, loop: asyncio.AbstractEventLoop) -> bool:
        """Return whether the current flush task is usable for the active loop.

        Args:
            loop: The currently running event loop for this worker process.

        Returns:
            True when the cached flush task belongs to the current live loop.
        """
        task = self._flush_task
        if task is None or task.done() or task.cancelled():
            return False

        try:
            task_loop = task.get_loop()
        except (AttributeError, RuntimeError):
            return False

        return task_loop is loop and task_loop.is_running()

    def _ensure_flush_task_started(self) -> None:
        """Best-effort lazy-start for the background flush task.

        In preloaded multi-worker deployments, the singleton may exist in a
        worker before the worker's startup hook has started the flush loop. A
        first buffered metric should recover from that state instead of sitting
        in memory forever.
        """
        if not self.recording_enabled or not self.enabled:
            return

        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            return

        if self._flush_task_is_active_for_loop(loop):
            return

        if self._shutdown_event.is_set():
            self._shutdown_event = asyncio.Event()

        self._flush_task = loop.create_task(self._flush_loop())
        logger.info("MetricsBufferService flush task started lazily")

    async def shutdown(self) -> None:
        """Shutdown service with final flush."""
        logger.info("MetricsBufferService shutting down...")

        # Signal shutdown
        self._shutdown_event.set()

        # Cancel the flush task
        if self._flush_task:
            self._flush_task.cancel()
            try:
                await self._flush_task
            except asyncio.CancelledError:
                pass

        # Final flush to persist any remaining metrics
        await self._flush_all()

        logger.info(f"MetricsBufferService shutdown complete: " f"total_buffered={self._total_buffered}, total_flushed={self._total_flushed}, " f"flush_count={self._flush_count}")

    def record_tool_metric(
        self,
        tool_id: str,
        start_time: float,
        success: bool,
        error_message: Optional[str] = None,
    ) -> None:
        """Buffer a tool metric for later flush.

        Args:
            tool_id: The UUID string of the tool.
            start_time: The monotonic start time of the invocation.
            success: True if the invocation succeeded.
            error_message: Error message if failed.
        """
        if not self.recording_enabled:
            return  # Execution metrics recording disabled
        if not self.enabled:
            # Fall back to immediate write
            self._write_tool_metric_immediately(tool_id, start_time, success, error_message)
            return

        metric = BufferedToolMetric(
            tool_id=tool_id,
            timestamp=datetime.now(timezone.utc),
            response_time=time.monotonic() - start_time,
            is_success=success,
            error_message=error_message,
        )

        self._ensure_flush_task_started()
        with self._lock:
            self._tool_metrics.append(metric)
            self._total_buffered += 1

    def record_tool_metric_with_duration(
        self,
        tool_id: str,
        response_time: float,
        success: bool,
        error_message: Optional[str] = None,
    ) -> None:
        """Buffer a tool metric with pre-calculated response time.

        Args:
            tool_id: UUID of the tool.
            response_time: Pre-calculated response time in seconds.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        if not self.recording_enabled:
            return  # Execution metrics recording disabled
        if not self.enabled:
            self._write_tool_metric_with_duration_immediately(tool_id, response_time, success, error_message)
            return

        metric = BufferedToolMetric(
            tool_id=tool_id,
            timestamp=datetime.now(timezone.utc),
            response_time=response_time,
            is_success=success,
            error_message=error_message,
        )

        self._ensure_flush_task_started()
        with self._lock:
            self._tool_metrics.append(metric)
            self._total_buffered += 1

    def record_resource_metric(
        self,
        resource_id: str,
        start_time: float,
        success: bool,
        error_message: Optional[str] = None,
    ) -> None:
        """Buffer a resource metric for later flush.

        Args:
            resource_id: UUID of the resource.
            start_time: Monotonic start time for response_time calculation.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        if not self.recording_enabled:
            return  # Execution metrics recording disabled
        if not self.enabled:
            self._write_resource_metric_immediately(resource_id, start_time, success, error_message)
            return

        metric = BufferedResourceMetric(
            resource_id=resource_id,
            timestamp=datetime.now(timezone.utc),
            response_time=time.monotonic() - start_time,
            is_success=success,
            error_message=error_message,
        )

        self._ensure_flush_task_started()
        with self._lock:
            self._resource_metrics.append(metric)
            self._total_buffered += 1

    def record_prompt_metric(
        self,
        prompt_id: str,
        start_time: float,
        success: bool,
        error_message: Optional[str] = None,
    ) -> None:
        """Buffer a prompt metric for later flush.

        Args:
            prompt_id: UUID of the prompt.
            start_time: Monotonic start time for response_time calculation.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        if not self.recording_enabled:
            return  # Execution metrics recording disabled
        if not self.enabled:
            self._write_prompt_metric_immediately(prompt_id, start_time, success, error_message)
            return

        metric = BufferedPromptMetric(
            prompt_id=prompt_id,
            timestamp=datetime.now(timezone.utc),
            response_time=time.monotonic() - start_time,
            is_success=success,
            error_message=error_message,
        )

        self._ensure_flush_task_started()
        with self._lock:
            self._prompt_metrics.append(metric)
            self._total_buffered += 1

    def record_server_metric(
        self,
        server_id: str,
        start_time: float,
        success: bool,
        error_message: Optional[str] = None,
    ) -> None:
        """Buffer a server metric for later flush.

        Args:
            server_id: UUID of the server.
            start_time: Monotonic start time for response_time calculation.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        if not self.recording_enabled:
            return  # Execution metrics recording disabled
        if not self.enabled:
            self._write_server_metric_immediately(server_id, start_time, success, error_message)
            return

        metric = BufferedServerMetric(
            server_id=server_id,
            timestamp=datetime.now(timezone.utc),
            response_time=time.monotonic() - start_time,
            is_success=success,
            error_message=error_message,
        )

        self._ensure_flush_task_started()
        with self._lock:
            self._server_metrics.append(metric)
            self._total_buffered += 1

    def record_server_metric_with_duration(
        self,
        server_id: str,
        response_time: float,
        success: bool,
        error_message: Optional[str] = None,
    ) -> None:
        """Buffer a server metric with pre-calculated response time.

        Args:
            server_id: UUID of the server.
            response_time: Pre-calculated response time in seconds.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        if not self.recording_enabled:
            return  # Execution metrics recording disabled
        if not self.enabled:
            self._write_server_metric_with_duration_immediately(server_id, response_time, success, error_message)
            return

        metric = BufferedServerMetric(
            server_id=server_id,
            timestamp=datetime.now(timezone.utc),
            response_time=response_time,
            is_success=success,
            error_message=error_message,
        )

        self._ensure_flush_task_started()
        with self._lock:
            self._server_metrics.append(metric)
            self._total_buffered += 1

    def record_a2a_agent_metric(
        self,
        a2a_agent_id: str,
        start_time: float,
        success: bool,
        interaction_type: str = "invoke",
        error_message: Optional[str] = None,
    ) -> None:
        """Buffer an A2A agent metric for later flush.

        Args:
            a2a_agent_id: UUID of the A2A agent.
            start_time: Monotonic start time for response_time calculation.
            success: Whether the operation succeeded.
            interaction_type: Type of interaction (e.g., "invoke").
            error_message: Optional error message if failed.
        """
        if not self.recording_enabled:
            return  # Execution metrics recording disabled
        if not self.enabled:
            self._write_a2a_agent_metric_immediately(a2a_agent_id, start_time, success, interaction_type, error_message)
            return

        metric = BufferedA2AAgentMetric(
            a2a_agent_id=a2a_agent_id,
            timestamp=datetime.now(timezone.utc),
            response_time=time.monotonic() - start_time,
            is_success=success,
            interaction_type=interaction_type,
            error_message=error_message,
        )

        self._ensure_flush_task_started()
        with self._lock:
            self._a2a_agent_metrics.append(metric)
            self._total_buffered += 1

    def record_a2a_agent_metric_with_duration(
        self,
        a2a_agent_id: str,
        response_time: float,
        success: bool,
        interaction_type: str = "invoke",
        error_message: Optional[str] = None,
    ) -> None:
        """Buffer an A2A agent metric with pre-calculated response time.

        Args:
            a2a_agent_id: UUID of the A2A agent.
            response_time: Pre-calculated response time in seconds.
            success: Whether the operation succeeded.
            interaction_type: Type of interaction (e.g., "invoke").
            error_message: Optional error message if failed.
        """
        if not self.recording_enabled:
            return  # Execution metrics recording disabled
        if not self.enabled:
            self._write_a2a_agent_metric_with_duration_immediately(a2a_agent_id, response_time, success, interaction_type, error_message)
            return

        metric = BufferedA2AAgentMetric(
            a2a_agent_id=a2a_agent_id,
            timestamp=datetime.now(timezone.utc),
            response_time=response_time,
            is_success=success,
            interaction_type=interaction_type,
            error_message=error_message,
        )

        self._ensure_flush_task_started()
        with self._lock:
            self._a2a_agent_metrics.append(metric)
            self._total_buffered += 1

    async def _flush_loop(self) -> None:
        """Background task that periodically flushes buffered metrics.

        Raises:
            asyncio.CancelledError: When the flush loop is cancelled.
        """
        logger.info(f"Metrics flush loop started (interval={self.flush_interval}s)")

        while not self._shutdown_event.is_set():
            try:
                # Wait for flush interval or shutdown
                try:
                    await asyncio.wait_for(
                        self._shutdown_event.wait(),
                        timeout=self.flush_interval,
                    )
                    # Shutdown signaled
                    break
                except asyncio.TimeoutError:
                    # Normal timeout, proceed to flush
                    pass

                await self._flush_all()

            except asyncio.CancelledError:
                logger.debug("Flush loop cancelled")
                raise
            except Exception as e:
                logger.error(f"Error in metrics flush loop: {e}", exc_info=True)
                # Continue the loop despite errors
                await asyncio.sleep(5)

    async def _flush_all(self) -> None:
        """Flush all buffered metrics to the database."""
        # Swap out buffers atomically
        with self._lock:
            tool_metrics = list(self._tool_metrics)
            resource_metrics = list(self._resource_metrics)
            prompt_metrics = list(self._prompt_metrics)
            server_metrics = list(self._server_metrics)
            a2a_agent_metrics = list(self._a2a_agent_metrics)
            self._tool_metrics.clear()
            self._resource_metrics.clear()
            self._prompt_metrics.clear()
            self._server_metrics.clear()
            self._a2a_agent_metrics.clear()

        total = len(tool_metrics) + len(resource_metrics) + len(prompt_metrics) + len(server_metrics) + len(a2a_agent_metrics)
        if total == 0:
            return

        logger.debug(
            f"Flushing {total} metrics: "
            f"tools={len(tool_metrics)}, resources={len(resource_metrics)}, prompts={len(prompt_metrics)}, "
            f"servers={len(server_metrics)}, a2a_agents={len(a2a_agent_metrics)}"
        )

        # Flush in thread to avoid blocking event loop
        await asyncio.to_thread(
            self._flush_to_db,
            tool_metrics,
            resource_metrics,
            prompt_metrics,
            server_metrics,
            a2a_agent_metrics,
        )

        self._total_flushed += total
        self._flush_count += 1

        logger.info(
            f"Metrics flush #{self._flush_count}: wrote {total} records "
            f"(tools={len(tool_metrics)}, resources={len(resource_metrics)}, prompts={len(prompt_metrics)}, "
            f"servers={len(server_metrics)}, a2a={len(a2a_agent_metrics)})"
        )

    def _flush_to_db(
        self,
        tool_metrics: list[BufferedToolMetric],
        resource_metrics: list[BufferedResourceMetric],
        prompt_metrics: list[BufferedPromptMetric],
        server_metrics: list[BufferedServerMetric],
        a2a_agent_metrics: list[BufferedA2AAgentMetric],
    ) -> None:
        """Write buffered metrics to database (runs in thread).

        Each metric type is written in its own transaction so that a
        foreign-key violation on one type (e.g., a ``tool_id`` whose row was
        deleted between buffering and flushing) does not roll back unrelated
        metrics.  ``fresh_db_session()`` commits on successful context exit
        and rolls back on exception, so explicit ``db.commit()`` is not
        needed inside the ``with`` blocks.

        Args:
            tool_metrics: List of buffered tool metrics to write.
            resource_metrics: List of buffered resource metrics to write.
            prompt_metrics: List of buffered prompt metrics to write.
            server_metrics: List of buffered server metrics to write.
            a2a_agent_metrics: List of buffered A2A agent metrics to write.
        """
        if tool_metrics:
            try:
                with fresh_db_session() as db:
                    db.bulk_insert_mappings(
                        ToolMetric,
                        [
                            {
                                "tool_id": m.tool_id,
                                "timestamp": m.timestamp,
                                "response_time": m.response_time,
                                "is_success": m.is_success,
                                "error_message": m.error_message,
                            }
                            for m in tool_metrics
                        ],
                    )
            except Exception as e:
                logger.error(f"Failed to flush tool metrics to database: {e}", exc_info=True)

        if resource_metrics:
            try:
                with fresh_db_session() as db:
                    db.bulk_insert_mappings(
                        ResourceMetric,
                        [
                            {
                                "resource_id": m.resource_id,
                                "timestamp": m.timestamp,
                                "response_time": m.response_time,
                                "is_success": m.is_success,
                                "error_message": m.error_message,
                            }
                            for m in resource_metrics
                        ],
                    )
            except Exception as e:
                logger.error(f"Failed to flush resource metrics to database: {e}", exc_info=True)

        if prompt_metrics:
            try:
                with fresh_db_session() as db:
                    db.bulk_insert_mappings(
                        PromptMetric,
                        [
                            {
                                "prompt_id": m.prompt_id,
                                "timestamp": m.timestamp,
                                "response_time": m.response_time,
                                "is_success": m.is_success,
                                "error_message": m.error_message,
                            }
                            for m in prompt_metrics
                        ],
                    )
            except Exception as e:
                logger.error(f"Failed to flush prompt metrics to database: {e}", exc_info=True)

        if a2a_agent_metrics:
            try:
                with fresh_db_session() as db:
                    db.bulk_insert_mappings(
                        A2AAgentMetric,
                        [
                            {
                                "a2a_agent_id": m.a2a_agent_id,
                                "timestamp": m.timestamp,
                                "response_time": m.response_time,
                                "is_success": m.is_success,
                                "interaction_type": m.interaction_type,
                                "error_message": m.error_message,
                            }
                            for m in a2a_agent_metrics
                        ],
                    )
            except Exception as e:
                logger.error(f"Failed to flush A2A agent metrics to database: {e}", exc_info=True)

        # ``server_id`` can originate from untrusted headers (X-Server-ID) in
        # admin API paths, so it may reference a nonexistent server.  Same
        # isolation guarantee as the other metric types above.
        if server_metrics:
            try:
                with fresh_db_session() as db:
                    db.bulk_insert_mappings(
                        ServerMetric,
                        [
                            {
                                "server_id": m.server_id,
                                "timestamp": m.timestamp,
                                "response_time": m.response_time,
                                "is_success": m.is_success,
                                "error_message": m.error_message,
                            }
                            for m in server_metrics
                        ],
                    )
            except Exception as e:
                logger.error(f"Failed to flush server metrics to database: {e}", exc_info=True)

    def _write_tool_metric_immediately(
        self,
        tool_id: str,
        start_time: float,
        success: bool,
        error_message: Optional[str],
    ) -> None:
        """Write a single tool metric immediately (fallback when buffering disabled).

        Args:
            tool_id: UUID of the tool.
            start_time: Monotonic start time for response_time calculation.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        try:
            with fresh_db_session() as db:
                metric = ToolMetric(
                    tool_id=tool_id,
                    timestamp=datetime.now(timezone.utc),
                    response_time=time.monotonic() - start_time,
                    is_success=success,
                    error_message=error_message,
                )
                db.add(metric)
        except Exception as e:
            logger.error(f"Failed to write tool metric: {e}")

    def _write_tool_metric_with_duration_immediately(
        self,
        tool_id: str,
        response_time: float,
        success: bool,
        error_message: Optional[str],
    ) -> None:
        """Write a single tool metric with pre-calculated duration immediately.

        Args:
            tool_id: UUID of the tool.
            response_time: Pre-calculated response time in seconds.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        try:
            with fresh_db_session() as db:
                metric = ToolMetric(
                    tool_id=tool_id,
                    timestamp=datetime.now(timezone.utc),
                    response_time=response_time,
                    is_success=success,
                    error_message=error_message,
                )
                db.add(metric)
        except Exception as e:
            logger.error(f"Failed to write tool metric: {e}")

    def _write_resource_metric_immediately(
        self,
        resource_id: str,
        start_time: float,
        success: bool,
        error_message: Optional[str],
    ) -> None:
        """Write a single resource metric immediately.

        Args:
            resource_id: UUID of the resource.
            start_time: Monotonic start time for response_time calculation.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        try:
            with fresh_db_session() as db:
                metric = ResourceMetric(
                    resource_id=resource_id,
                    timestamp=datetime.now(timezone.utc),
                    response_time=time.monotonic() - start_time,
                    is_success=success,
                    error_message=error_message,
                )
                db.add(metric)
        except Exception as e:
            logger.error(f"Failed to write resource metric: {e}")

    def _write_prompt_metric_immediately(
        self,
        prompt_id: str,
        start_time: float,
        success: bool,
        error_message: Optional[str],
    ) -> None:
        """Write a single prompt metric immediately.

        Args:
            prompt_id: UUID of the prompt.
            start_time: Monotonic start time for response_time calculation.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        try:
            with fresh_db_session() as db:
                metric = PromptMetric(
                    prompt_id=prompt_id,
                    timestamp=datetime.now(timezone.utc),
                    response_time=time.monotonic() - start_time,
                    is_success=success,
                    error_message=error_message,
                )
                db.add(metric)
        except Exception as e:
            logger.error(f"Failed to write prompt metric: {e}")

    def _write_server_metric_immediately(
        self,
        server_id: str,
        start_time: float,
        success: bool,
        error_message: Optional[str],
    ) -> None:
        """Write a single server metric immediately.

        Args:
            server_id: UUID of the server.
            start_time: Monotonic start time for response_time calculation.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        try:
            with fresh_db_session() as db:
                metric = ServerMetric(
                    server_id=server_id,
                    timestamp=datetime.now(timezone.utc),
                    response_time=time.monotonic() - start_time,
                    is_success=success,
                    error_message=error_message,
                )
                db.add(metric)
        except Exception as e:
            logger.error(f"Failed to write server metric: {e}")

    def _write_server_metric_with_duration_immediately(
        self,
        server_id: str,
        response_time: float,
        success: bool,
        error_message: Optional[str],
    ) -> None:
        """Write a single server metric with pre-calculated duration immediately.

        Args:
            server_id: UUID of the server.
            response_time: Pre-calculated response time in seconds.
            success: Whether the operation succeeded.
            error_message: Optional error message if failed.
        """
        try:
            with fresh_db_session() as db:
                metric = ServerMetric(
                    server_id=server_id,
                    timestamp=datetime.now(timezone.utc),
                    response_time=response_time,
                    is_success=success,
                    error_message=error_message,
                )
                db.add(metric)
        except Exception as e:
            logger.error(f"Failed to write server metric: {e}")

    def _write_a2a_agent_metric_immediately(
        self,
        a2a_agent_id: str,
        start_time: float,
        success: bool,
        interaction_type: str,
        error_message: Optional[str],
    ) -> None:
        """Write a single A2A agent metric immediately.

        Args:
            a2a_agent_id: UUID of the A2A agent.
            start_time: Monotonic start time for response_time calculation.
            success: Whether the operation succeeded.
            interaction_type: Type of interaction (e.g., "invoke").
            error_message: Optional error message if failed.
        """
        try:
            with fresh_db_session() as db:
                metric = A2AAgentMetric(
                    a2a_agent_id=a2a_agent_id,
                    timestamp=datetime.now(timezone.utc),
                    response_time=time.monotonic() - start_time,
                    is_success=success,
                    interaction_type=interaction_type,
                    error_message=error_message,
                )
                db.add(metric)
        except Exception as e:
            logger.error(f"Failed to write A2A agent metric: {e}")

    def _write_a2a_agent_metric_with_duration_immediately(
        self,
        a2a_agent_id: str,
        response_time: float,
        success: bool,
        interaction_type: str,
        error_message: Optional[str],
    ) -> None:
        """Write a single A2A agent metric with pre-calculated duration immediately.

        Args:
            a2a_agent_id: UUID of the A2A agent.
            response_time: Pre-calculated response time in seconds.
            success: Whether the operation succeeded.
            interaction_type: Type of interaction (e.g., "invoke").
            error_message: Optional error message if failed.
        """
        try:
            with fresh_db_session() as db:
                metric = A2AAgentMetric(
                    a2a_agent_id=a2a_agent_id,
                    timestamp=datetime.now(timezone.utc),
                    response_time=response_time,
                    is_success=success,
                    interaction_type=interaction_type,
                    error_message=error_message,
                )
                db.add(metric)
        except Exception as e:
            logger.error(f"Failed to write A2A agent metric: {e}")

    def get_stats(self) -> dict:
        """Get buffer statistics for monitoring.

        Returns:
            dict: Buffer statistics including enabled state, sizes, and counts.
        """
        with self._lock:
            current_size = len(self._tool_metrics) + len(self._resource_metrics) + len(self._prompt_metrics) + len(self._server_metrics) + len(self._a2a_agent_metrics)

        return {
            "recording_enabled": self.recording_enabled,
            "enabled": self.enabled,
            "flush_interval": self.flush_interval,
            "max_buffer_size": self.max_buffer_size,
            "current_buffer_size": current_size,
            "total_buffered": self._total_buffered,
            "total_flushed": self._total_flushed,
            "flush_count": self._flush_count,
        }


# Singleton instance
_metrics_buffer_service: Optional[MetricsBufferService] = None


def get_metrics_buffer_service() -> MetricsBufferService:
    """Get or create the singleton MetricsBufferService instance.

    Returns:
        MetricsBufferService: The singleton metrics buffer service instance.
    """
    global _metrics_buffer_service  # pylint: disable=global-statement
    if _metrics_buffer_service is None:
        _metrics_buffer_service = MetricsBufferService()
    return _metrics_buffer_service
