# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Familiar Metrics Collection (Phase 3 - v2.5.0)

Provides metrics collection for monitoring:
- Tool execution time and success rates
- LLM call latency and token usage
- System resource utilization
- Custom application metrics

Usage:
    from familiar.core.metrics import (
        get_metrics_collector, record_tool_execution,
        record_llm_call, ToolMetrics
    )

    # Record tool execution
    with record_tool_execution("read_file") as ctx:
        result = execute_tool()
        ctx.set_success(True)

    # Get metrics summary
    metrics = get_metrics_collector()
    summary = metrics.get_tool_summary("read_file")
"""

import logging
import statistics
import threading
import time
from collections import defaultdict
from contextlib import contextmanager
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Callable, Dict, List, Optional

logger = logging.getLogger(__name__)


# =============================================================================
# ENUMS
# =============================================================================


class MetricType(str, Enum):
    """Types of metrics."""

    COUNTER = "counter"
    GAUGE = "gauge"
    HISTOGRAM = "histogram"
    SUMMARY = "summary"


class AlertSeverity(str, Enum):
    """Alert severity levels."""

    INFO = "info"
    WARNING = "warning"
    CRITICAL = "critical"


# =============================================================================
# DATA CLASSES
# =============================================================================


@dataclass
class ToolExecutionRecord:
    """Record of a single tool execution."""

    tool_name: str
    start_time: float
    end_time: float
    duration_ms: float
    success: bool
    error_message: Optional[str] = None
    input_size: int = 0
    output_size: int = 0
    user_id: Optional[str] = None

    @property
    def timestamp(self) -> datetime:
        return datetime.fromtimestamp(self.start_time)


@dataclass
class ToolMetrics:
    """Aggregated metrics for a tool."""

    tool_name: str
    total_calls: int = 0
    successful_calls: int = 0
    failed_calls: int = 0
    total_duration_ms: float = 0.0
    min_duration_ms: float = float("inf")
    max_duration_ms: float = 0.0
    durations: List[float] = field(default_factory=list)
    last_execution: Optional[datetime] = None
    last_error: Optional[str] = None
    timeout_count: int = 0

    @property
    def success_rate(self) -> float:
        if self.total_calls == 0:
            return 0.0
        return self.successful_calls / self.total_calls

    @property
    def failure_rate(self) -> float:
        return 1.0 - self.success_rate

    @property
    def avg_duration_ms(self) -> float:
        if self.total_calls == 0:
            return 0.0
        return self.total_duration_ms / self.total_calls

    @property
    def p50_duration_ms(self) -> float:
        if not self.durations:
            return 0.0
        return statistics.median(self.durations)

    @property
    def p95_duration_ms(self) -> float:
        if len(self.durations) < 2:
            return self.max_duration_ms
        sorted_durations = sorted(self.durations)
        idx = int(len(sorted_durations) * 0.95)
        return sorted_durations[min(idx, len(sorted_durations) - 1)]

    @property
    def p99_duration_ms(self) -> float:
        if len(self.durations) < 2:
            return self.max_duration_ms
        sorted_durations = sorted(self.durations)
        idx = int(len(sorted_durations) * 0.99)
        return sorted_durations[min(idx, len(sorted_durations) - 1)]

    def record(self, record: ToolExecutionRecord) -> None:
        """Add an execution record to the metrics."""
        self.total_calls += 1
        self.total_duration_ms += record.duration_ms
        self.min_duration_ms = min(self.min_duration_ms, record.duration_ms)
        self.max_duration_ms = max(self.max_duration_ms, record.duration_ms)
        self.last_execution = record.timestamp

        # Keep last 1000 durations for percentile calculations
        self.durations.append(record.duration_ms)
        if len(self.durations) > 1000:
            self.durations.pop(0)

        if record.success:
            self.successful_calls += 1
        else:
            self.failed_calls += 1
            self.last_error = record.error_message
            if record.error_message and (
                "timeout" in record.error_message.lower()
                or "timed out" in record.error_message.lower()
            ):
                self.timeout_count += 1

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "tool_name": self.tool_name,
            "total_calls": self.total_calls,
            "successful_calls": self.successful_calls,
            "failed_calls": self.failed_calls,
            "success_rate": round(self.success_rate, 4),
            "avg_duration_ms": round(self.avg_duration_ms, 2),
            "min_duration_ms": round(self.min_duration_ms, 2)
            if self.min_duration_ms != float("inf")
            else 0,
            "max_duration_ms": round(self.max_duration_ms, 2),
            "p50_duration_ms": round(self.p50_duration_ms, 2),
            "p95_duration_ms": round(self.p95_duration_ms, 2),
            "p99_duration_ms": round(self.p99_duration_ms, 2),
            "timeout_count": self.timeout_count,
            "last_execution": self.last_execution.isoformat() if self.last_execution else None,
            "last_error": self.last_error,
        }


@dataclass
class LLMCallRecord:
    """Record of a single LLM API call."""

    provider: str
    model: str
    start_time: float
    end_time: float
    duration_ms: float
    success: bool
    prompt_tokens: int = 0
    completion_tokens: int = 0
    total_tokens: int = 0
    cost_usd: float = 0.0
    error_message: Optional[str] = None
    user_id: Optional[str] = None
    trace_id: Optional[str] = None

    @property
    def timestamp(self) -> datetime:
        return datetime.fromtimestamp(self.start_time)


@dataclass
class LLMMetrics:
    """Aggregated metrics for an LLM provider/model."""

    provider: str
    model: str
    total_calls: int = 0
    successful_calls: int = 0
    failed_calls: int = 0
    total_duration_ms: float = 0.0
    total_prompt_tokens: int = 0
    total_completion_tokens: int = 0
    total_cost_usd: float = 0.0
    durations: List[float] = field(default_factory=list)
    rate_limit_errors: int = 0
    auth_errors: int = 0
    last_call: Optional[datetime] = None
    last_error: Optional[str] = None

    @property
    def success_rate(self) -> float:
        if self.total_calls == 0:
            return 0.0
        return self.successful_calls / self.total_calls

    @property
    def avg_duration_ms(self) -> float:
        if self.total_calls == 0:
            return 0.0
        return self.total_duration_ms / self.total_calls

    @property
    def avg_tokens_per_call(self) -> float:
        if self.successful_calls == 0:
            return 0.0
        return (self.total_prompt_tokens + self.total_completion_tokens) / self.successful_calls

    @property
    def avg_cost_per_call(self) -> float:
        if self.successful_calls == 0:
            return 0.0
        return self.total_cost_usd / self.successful_calls

    @property
    def p95_duration_ms(self) -> float:
        if len(self.durations) < 2:
            return self.avg_duration_ms
        sorted_durations = sorted(self.durations)
        idx = int(len(sorted_durations) * 0.95)
        return sorted_durations[min(idx, len(sorted_durations) - 1)]

    def record(self, record: LLMCallRecord) -> None:
        """Add a call record to the metrics."""
        self.total_calls += 1
        self.total_duration_ms += record.duration_ms
        self.last_call = record.timestamp

        # Keep last 1000 durations
        self.durations.append(record.duration_ms)
        if len(self.durations) > 1000:
            self.durations.pop(0)

        if record.success:
            self.successful_calls += 1
            self.total_prompt_tokens += record.prompt_tokens
            self.total_completion_tokens += record.completion_tokens
            self.total_cost_usd += record.cost_usd
        else:
            self.failed_calls += 1
            self.last_error = record.error_message
            if record.error_message:
                if "rate limit" in record.error_message.lower():
                    self.rate_limit_errors += 1
                elif "auth" in record.error_message.lower() or "401" in record.error_message:
                    self.auth_errors += 1

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "provider": self.provider,
            "model": self.model,
            "total_calls": self.total_calls,
            "successful_calls": self.successful_calls,
            "failed_calls": self.failed_calls,
            "success_rate": round(self.success_rate, 4),
            "avg_duration_ms": round(self.avg_duration_ms, 2),
            "p95_duration_ms": round(self.p95_duration_ms, 2),
            "total_prompt_tokens": self.total_prompt_tokens,
            "total_completion_tokens": self.total_completion_tokens,
            "total_cost_usd": round(self.total_cost_usd, 4),
            "avg_tokens_per_call": round(self.avg_tokens_per_call, 1),
            "avg_cost_per_call": round(self.avg_cost_per_call, 6),
            "rate_limit_errors": self.rate_limit_errors,
            "auth_errors": self.auth_errors,
            "last_call": self.last_call.isoformat() if self.last_call else None,
            "last_error": self.last_error,
        }


@dataclass
class MetricAlert:
    """An alert triggered by a metric threshold."""

    name: str
    severity: AlertSeverity
    message: str
    metric_name: str
    threshold: float
    current_value: float
    timestamp: datetime = field(default_factory=datetime.now)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "severity": self.severity.value,
            "message": self.message,
            "metric_name": self.metric_name,
            "threshold": self.threshold,
            "current_value": self.current_value,
            "timestamp": self.timestamp.isoformat(),
        }


# =============================================================================
# EXECUTION CONTEXT
# =============================================================================


class ToolExecutionContext:
    """Context manager helper for tracking tool execution."""

    def __init__(self, tool_name: str, collector: "MetricsCollector"):
        self.tool_name = tool_name
        self.collector = collector
        self.start_time = 0.0
        self.success = True
        self.error_message: Optional[str] = None
        self.input_size = 0
        self.output_size = 0
        self.user_id: Optional[str] = None

    def set_success(self, success: bool) -> None:
        self.success = success

    def set_error(self, message: str) -> None:
        self.success = False
        self.error_message = message

    def set_input_size(self, size: int) -> None:
        self.input_size = size

    def set_output_size(self, size: int) -> None:
        self.output_size = size

    def set_user_id(self, user_id: str) -> None:
        self.user_id = user_id


class LLMCallContext:
    """Context manager helper for tracking LLM calls."""

    def __init__(self, provider: str, model: str, collector: "MetricsCollector"):
        self.provider = provider
        self.model = model
        self.collector = collector
        self.start_time = 0.0
        self.success = True
        self.error_message: Optional[str] = None
        self.prompt_tokens = 0
        self.completion_tokens = 0
        self.cost_usd = 0.0
        self.user_id: Optional[str] = None
        self.trace_id: Optional[str] = None

    def set_success(self, success: bool) -> None:
        self.success = success

    def set_error(self, message: str) -> None:
        self.success = False
        self.error_message = message

    def set_tokens(self, prompt: int, completion: int) -> None:
        self.prompt_tokens = prompt
        self.completion_tokens = completion

    def set_cost(self, cost_usd: float) -> None:
        self.cost_usd = cost_usd

    def set_user_id(self, user_id: str) -> None:
        self.user_id = user_id

    def set_trace_id(self, trace_id: str) -> None:
        self.trace_id = trace_id


# =============================================================================
# METRICS COLLECTOR
# =============================================================================


class MetricsCollector:
    """
    Central collector for all application metrics.

    Thread-safe singleton that collects:
    - Tool execution metrics
    - LLM call metrics
    - Custom application metrics
    - Alert tracking
    """

    def __init__(self):
        self._lock = threading.RLock()

        # Tool metrics
        self._tool_metrics: Dict[str, ToolMetrics] = {}
        self._tool_records: List[ToolExecutionRecord] = []

        # LLM metrics
        self._llm_metrics: Dict[str, LLMMetrics] = {}  # key: "provider:model"
        self._llm_records: List[LLMCallRecord] = []

        # Custom metrics
        self._counters: Dict[str, int] = defaultdict(int)
        self._gauges: Dict[str, float] = {}
        self._histograms: Dict[str, List[float]] = defaultdict(list)

        # Alerts
        self._alerts: List[MetricAlert] = []
        self._alert_callbacks: List[Callable[[MetricAlert], None]] = []

        # Alert thresholds
        self._alert_thresholds: Dict[str, Dict[str, Any]] = {
            "tool_failure_rate": {
                "threshold": 0.1,  # 10% failure rate
                "severity": AlertSeverity.WARNING,
            },
            "tool_timeout_rate": {
                "threshold": 0.05,  # 5% timeout rate
                "severity": AlertSeverity.WARNING,
            },
            "llm_error_rate": {
                "threshold": 0.1,  # 10% error rate
                "severity": AlertSeverity.WARNING,
            },
            "llm_latency_p95_ms": {
                "threshold": 30000,  # 30 seconds
                "severity": AlertSeverity.WARNING,
            },
        }

        # History limits
        self._max_records = 10000

    # -------------------------------------------------------------------------
    # TOOL METRICS
    # -------------------------------------------------------------------------

    def record_tool_execution(
        self,
        tool_name: str,
        duration_ms: float,
        success: bool,
        error_message: Optional[str] = None,
        input_size: int = 0,
        output_size: int = 0,
        user_id: Optional[str] = None,
    ) -> None:
        """Record a tool execution."""
        with self._lock:
            now = time.time()
            record = ToolExecutionRecord(
                tool_name=tool_name,
                start_time=now - (duration_ms / 1000),
                end_time=now,
                duration_ms=duration_ms,
                success=success,
                error_message=error_message,
                input_size=input_size,
                output_size=output_size,
                user_id=user_id,
            )

            # Update aggregated metrics
            if tool_name not in self._tool_metrics:
                self._tool_metrics[tool_name] = ToolMetrics(tool_name=tool_name)
            self._tool_metrics[tool_name].record(record)

            # Store record
            self._tool_records.append(record)
            if len(self._tool_records) > self._max_records:
                self._tool_records.pop(0)

            # Check alerts
            self._check_tool_alerts(tool_name)

    @contextmanager
    def track_tool_execution(self, tool_name: str):
        """Context manager for tracking tool execution."""
        ctx = ToolExecutionContext(tool_name, self)
        ctx.start_time = time.time()

        try:
            yield ctx
        except Exception as e:
            ctx.set_error(str(e))
            raise
        finally:
            end_time = time.time()
            duration_ms = (end_time - ctx.start_time) * 1000
            self.record_tool_execution(
                tool_name=tool_name,
                duration_ms=duration_ms,
                success=ctx.success,
                error_message=ctx.error_message,
                input_size=ctx.input_size,
                output_size=ctx.output_size,
                user_id=ctx.user_id,
            )

    def get_tool_metrics(self, tool_name: str) -> Optional[ToolMetrics]:
        """Get metrics for a specific tool."""
        with self._lock:
            return self._tool_metrics.get(tool_name)

    def get_all_tool_metrics(self) -> Dict[str, ToolMetrics]:
        """Get metrics for all tools."""
        with self._lock:
            return dict(self._tool_metrics)

    def get_tool_summary(self) -> Dict[str, Any]:
        """Get summary of all tool metrics."""
        with self._lock:
            tools = {}
            for name, metrics in self._tool_metrics.items():
                tools[name] = metrics.to_dict()

            # Calculate totals
            total_calls = sum(m.total_calls for m in self._tool_metrics.values())
            total_failures = sum(m.failed_calls for m in self._tool_metrics.values())

            return {
                "tools": tools,
                "total_calls": total_calls,
                "total_failures": total_failures,
                "overall_success_rate": (total_calls - total_failures) / total_calls
                if total_calls > 0
                else 0,
                "tool_count": len(self._tool_metrics),
            }

    # -------------------------------------------------------------------------
    # LLM METRICS
    # -------------------------------------------------------------------------

    def record_llm_call(
        self,
        provider: str,
        model: str,
        duration_ms: float,
        success: bool,
        prompt_tokens: int = 0,
        completion_tokens: int = 0,
        cost_usd: float = 0.0,
        error_message: Optional[str] = None,
        user_id: Optional[str] = None,
        trace_id: Optional[str] = None,
    ) -> None:
        """Record an LLM API call."""
        with self._lock:
            now = time.time()
            record = LLMCallRecord(
                provider=provider,
                model=model,
                start_time=now - (duration_ms / 1000),
                end_time=now,
                duration_ms=duration_ms,
                success=success,
                prompt_tokens=prompt_tokens,
                completion_tokens=completion_tokens,
                total_tokens=prompt_tokens + completion_tokens,
                cost_usd=cost_usd,
                error_message=error_message,
                user_id=user_id,
                trace_id=trace_id,
            )

            # Update aggregated metrics
            key = f"{provider}:{model}"
            if key not in self._llm_metrics:
                self._llm_metrics[key] = LLMMetrics(provider=provider, model=model)
            self._llm_metrics[key].record(record)

            # Store record
            self._llm_records.append(record)
            if len(self._llm_records) > self._max_records:
                self._llm_records.pop(0)

            # Check alerts
            self._check_llm_alerts(key)

    @contextmanager
    def track_llm_call(self, provider: str, model: str):
        """Context manager for tracking LLM calls."""
        ctx = LLMCallContext(provider, model, self)
        ctx.start_time = time.time()

        try:
            yield ctx
        except Exception as e:
            ctx.set_error(str(e))
            raise
        finally:
            end_time = time.time()
            duration_ms = (end_time - ctx.start_time) * 1000
            self.record_llm_call(
                provider=provider,
                model=model,
                duration_ms=duration_ms,
                success=ctx.success,
                prompt_tokens=ctx.prompt_tokens,
                completion_tokens=ctx.completion_tokens,
                cost_usd=ctx.cost_usd,
                error_message=ctx.error_message,
                user_id=ctx.user_id,
                trace_id=ctx.trace_id,
            )

    def get_llm_metrics(self, provider: str, model: str) -> Optional[LLMMetrics]:
        """Get metrics for a specific provider/model."""
        with self._lock:
            return self._llm_metrics.get(f"{provider}:{model}")

    def get_all_llm_metrics(self) -> Dict[str, LLMMetrics]:
        """Get metrics for all LLM providers/models."""
        with self._lock:
            return dict(self._llm_metrics)

    def get_llm_summary(self) -> Dict[str, Any]:
        """Get summary of all LLM metrics."""
        with self._lock:
            providers = {}
            for key, metrics in self._llm_metrics.items():
                providers[key] = metrics.to_dict()

            # Calculate totals
            total_calls = sum(m.total_calls for m in self._llm_metrics.values())
            total_tokens = sum(
                m.total_prompt_tokens + m.total_completion_tokens
                for m in self._llm_metrics.values()
            )
            total_cost = sum(m.total_cost_usd for m in self._llm_metrics.values())

            return {
                "providers": providers,
                "total_calls": total_calls,
                "total_tokens": total_tokens,
                "total_cost_usd": round(total_cost, 4),
                "provider_count": len(self._llm_metrics),
            }

    # -------------------------------------------------------------------------
    # CUSTOM METRICS
    # -------------------------------------------------------------------------

    def increment_counter(self, name: str, value: int = 1) -> None:
        """Increment a counter metric."""
        with self._lock:
            self._counters[name] += value

    def set_gauge(self, name: str, value: float) -> None:
        """Set a gauge metric."""
        with self._lock:
            self._gauges[name] = value

    def record_histogram(self, name: str, value: float) -> None:
        """Record a value in a histogram."""
        with self._lock:
            self._histograms[name].append(value)
            if len(self._histograms[name]) > 1000:
                self._histograms[name].pop(0)

    def get_counter(self, name: str) -> int:
        """Get a counter value."""
        with self._lock:
            return self._counters.get(name, 0)

    def get_gauge(self, name: str) -> Optional[float]:
        """Get a gauge value."""
        with self._lock:
            return self._gauges.get(name)

    def get_histogram_stats(self, name: str) -> Dict[str, float]:
        """Get histogram statistics."""
        with self._lock:
            values = self._histograms.get(name, [])
            if not values:
                return {"count": 0}

            sorted_values = sorted(values)
            return {
                "count": len(values),
                "min": min(values),
                "max": max(values),
                "avg": statistics.mean(values),
                "p50": statistics.median(values),
                "p95": sorted_values[int(len(sorted_values) * 0.95)]
                if len(sorted_values) >= 2
                else max(values),
                "p99": sorted_values[int(len(sorted_values) * 0.99)]
                if len(sorted_values) >= 2
                else max(values),
            }

    # -------------------------------------------------------------------------
    # ALERTS
    # -------------------------------------------------------------------------

    def set_alert_threshold(
        self,
        metric_name: str,
        threshold: float,
        severity: AlertSeverity = AlertSeverity.WARNING,
    ) -> None:
        """Set an alert threshold."""
        with self._lock:
            self._alert_thresholds[metric_name] = {
                "threshold": threshold,
                "severity": severity,
            }

    def add_alert_callback(self, callback: Callable[[MetricAlert], None]) -> None:
        """Add a callback for when alerts are triggered."""
        with self._lock:
            self._alert_callbacks.append(callback)

    def get_alerts(self, since: Optional[datetime] = None) -> List[MetricAlert]:
        """Get alerts, optionally filtered by time."""
        with self._lock:
            if since is None:
                return list(self._alerts)
            return [a for a in self._alerts if a.timestamp >= since]

    def clear_alerts(self) -> None:
        """Clear all alerts."""
        with self._lock:
            self._alerts.clear()

    def _check_tool_alerts(self, tool_name: str) -> None:
        """Check and trigger tool-related alerts."""
        metrics = self._tool_metrics.get(tool_name)
        if not metrics or metrics.total_calls < 10:
            return

        # Check failure rate
        if metrics.failure_rate > self._alert_thresholds.get("tool_failure_rate", {}).get(
            "threshold", 0.1
        ):
            self._trigger_alert(
                name=f"high_failure_rate_{tool_name}",
                severity=self._alert_thresholds["tool_failure_rate"]["severity"],
                message=f"Tool '{tool_name}' has high failure rate: {metrics.failure_rate:.1%}",
                metric_name="tool_failure_rate",
                threshold=self._alert_thresholds["tool_failure_rate"]["threshold"],
                current_value=metrics.failure_rate,
            )

        # Check timeout rate
        if metrics.total_calls > 0:
            timeout_rate = metrics.timeout_count / metrics.total_calls
            if timeout_rate > self._alert_thresholds.get("tool_timeout_rate", {}).get(
                "threshold", 0.05
            ):
                self._trigger_alert(
                    name=f"high_timeout_rate_{tool_name}",
                    severity=self._alert_thresholds["tool_timeout_rate"]["severity"],
                    message=f"Tool '{tool_name}' has high timeout rate: {timeout_rate:.1%}",
                    metric_name="tool_timeout_rate",
                    threshold=self._alert_thresholds["tool_timeout_rate"]["threshold"],
                    current_value=timeout_rate,
                )

    def _check_llm_alerts(self, provider_key: str) -> None:
        """Check and trigger LLM-related alerts."""
        metrics = self._llm_metrics.get(provider_key)
        if not metrics or metrics.total_calls < 10:
            return

        # Check error rate
        error_rate = 1.0 - metrics.success_rate
        if error_rate > self._alert_thresholds.get("llm_error_rate", {}).get("threshold", 0.1):
            self._trigger_alert(
                name=f"high_llm_error_rate_{provider_key}",
                severity=self._alert_thresholds["llm_error_rate"]["severity"],
                message=f"LLM '{provider_key}' has high error rate: {error_rate:.1%}",
                metric_name="llm_error_rate",
                threshold=self._alert_thresholds["llm_error_rate"]["threshold"],
                current_value=error_rate,
            )

        # Check latency
        if metrics.p95_duration_ms > self._alert_thresholds.get("llm_latency_p95_ms", {}).get(
            "threshold", 30000
        ):
            self._trigger_alert(
                name=f"high_llm_latency_{provider_key}",
                severity=self._alert_thresholds["llm_latency_p95_ms"]["severity"],
                message=f"LLM '{provider_key}' has high p95 latency: {metrics.p95_duration_ms:.0f}ms",
                metric_name="llm_latency_p95_ms",
                threshold=self._alert_thresholds["llm_latency_p95_ms"]["threshold"],
                current_value=metrics.p95_duration_ms,
            )

    def _trigger_alert(
        self,
        name: str,
        severity: AlertSeverity,
        message: str,
        metric_name: str,
        threshold: float,
        current_value: float,
    ) -> None:
        """Trigger an alert."""
        # Deduplicate recent alerts (within 5 minutes)
        cutoff = datetime.now() - timedelta(minutes=5)
        recent = [a for a in self._alerts if a.name == name and a.timestamp > cutoff]
        if recent:
            return

        alert = MetricAlert(
            name=name,
            severity=severity,
            message=message,
            metric_name=metric_name,
            threshold=threshold,
            current_value=current_value,
        )

        self._alerts.append(alert)
        if len(self._alerts) > 1000:
            self._alerts.pop(0)

        # Log alert
        if severity == AlertSeverity.CRITICAL:
            logger.critical(f"ALERT: {message}")
        elif severity == AlertSeverity.WARNING:
            logger.warning(f"ALERT: {message}")
        else:
            logger.info(f"ALERT: {message}")

        # Call callbacks
        for callback in self._alert_callbacks:
            try:
                callback(alert)
            except Exception as e:
                logger.error(f"Alert callback failed: {e}")

    # -------------------------------------------------------------------------
    # EXPORT
    # -------------------------------------------------------------------------

    def get_full_summary(self) -> Dict[str, Any]:
        """Get complete metrics summary."""
        with self._lock:
            return {
                "tools": self.get_tool_summary(),
                "llm": self.get_llm_summary(),
                "counters": dict(self._counters),
                "gauges": dict(self._gauges),
                "alerts": [a.to_dict() for a in self._alerts[-100:]],  # Last 100 alerts
                "timestamp": datetime.now().isoformat(),
            }

    def reset(self) -> None:
        """Reset all metrics (for testing)."""
        with self._lock:
            self._tool_metrics.clear()
            self._tool_records.clear()
            self._llm_metrics.clear()
            self._llm_records.clear()
            self._counters.clear()
            self._gauges.clear()
            self._histograms.clear()
            self._alerts.clear()


# =============================================================================
# SINGLETON
# =============================================================================

_metrics_collector: Optional[MetricsCollector] = None
_metrics_lock = threading.Lock()


def get_metrics_collector() -> MetricsCollector:
    """Get the singleton metrics collector."""
    global _metrics_collector
    if _metrics_collector is None:
        with _metrics_lock:
            if _metrics_collector is None:
                _metrics_collector = MetricsCollector()
    return _metrics_collector


def reset_metrics_collector() -> None:
    """Reset the metrics collector (for testing)."""
    global _metrics_collector
    with _metrics_lock:
        if _metrics_collector:
            _metrics_collector.reset()
        _metrics_collector = None


# =============================================================================
# CONVENIENCE FUNCTIONS
# =============================================================================


@contextmanager
def record_tool_execution(tool_name: str):
    """Convenience function for tracking tool execution."""
    collector = get_metrics_collector()
    with collector.track_tool_execution(tool_name) as ctx:
        yield ctx


@contextmanager
def record_llm_call(provider: str, model: str):
    """Convenience function for tracking LLM calls."""
    collector = get_metrics_collector()
    with collector.track_llm_call(provider, model) as ctx:
        yield ctx
