"""
Async-first performance metrics collection for Fapilog v3.
Implements minimal Prometheus-compatible counters and histograms used by
parallel processing and plugin execution paths.

Design goals:
- Pure async/await, no blocking I/O
- Zero global state; instances are container-scoped
- Safe no-op behavior when metrics are disabled by settings
"""

from __future__ import annotations

import asyncio
from dataclasses import dataclass
from time import perf_counter
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:  # pragma: no cover - type checking only
    pass

CollectorRegistry: Any
Counter: Any
Gauge: Any
Histogram: Any

try:
    from prometheus_client import CollectorRegistry as _CR
    from prometheus_client import Counter as _C
    from prometheus_client import Gauge as _G
    from prometheus_client import Histogram as _H

    CollectorRegistry, Counter, Gauge, Histogram = _CR, _C, _G, _H
    _PROMETHEUS_AVAILABLE = True
except Exception:  # pragma: no cover - handled via graceful fallback
    CollectorRegistry = Counter = Gauge = Histogram = None
    _PROMETHEUS_AVAILABLE = False


@dataclass
class PipelineMetrics:
    """Captured runtime metrics for quick assertions in tests."""

    events_processed: int = 0
    events_filtered: int = 0
    plugin_errors: int = 0
    # Consumers can fetch per-plugin profiles via MetricsCollector APIs


@dataclass
class PluginStats:
    """In-memory per-plugin execution statistics for profiling and QA.

    These stats are container-scoped and maintained even when exporters are
    disabled so that tests and validation utilities can assert performance.
    """

    executions: int = 0
    errors: int = 0
    total_duration_seconds: float = 0.0

    # Average can be computed by consumers as
    # total_duration_seconds / executions when needed.


class MetricsCollector:
    """Container-scoped async metrics collector.

    If Prometheus client is unavailable or metrics are disabled, all methods
    are safe no-ops while still tracking basic in-memory counters for tests.
    """

    def __init__(self, *, enabled: bool = False) -> None:
        self._prom_available = _PROMETHEUS_AVAILABLE
        if enabled and not self._prom_available:
            try:
                from ..core import diagnostics as _diag

                _diag.warn(
                    "metrics",
                    "prometheus_client not installed; disabling exporter",
                )
            except Exception:
                # Best-effort warning only
                pass
        self._enabled = bool(enabled and self._prom_available)
        self._lock = asyncio.Lock()
        self._state = PipelineMetrics()
        self._size_guard_truncated = 0
        self._size_guard_dropped = 0
        # Per-plugin profiling stats (container scoped)
        self._plugin_stats: dict[str, PluginStats] = {}

        # Lazily-initialized exporters to avoid global registration noise
        self._c_events: Any | None = None
        self._c_plugin_errors: Any | None = None
        self._h_process_latency: Any | None = None
        self._h_plugin_latency: Any | None = None
        self._registry: CollectorRegistry | None = None

        # Additional pipeline metrics (lazy)
        self._c_events_submitted: Any | None = None
        self._c_events_dropped: Any | None = None
        self._c_events_filtered: Any | None = None
        self._c_backpressure_waits: Any | None = None
        self._h_flush_latency: Any | None = None
        self._h_batch_size: Any | None = None
        self._c_sink_errors: Any | None = None
        self._g_queue_high_watermark: Any | None = None
        self._g_filter_sample_rate: Any | None = None
        self._g_rate_limit_keys: Any | None = None
        self._c_size_guard_truncated: Any | None = None
        self._c_size_guard_dropped: Any | None = None
        self._c_redaction_exceptions: Any | None = None

        if self._enabled:
            # Minimal metric set; names align with conventional Prometheus
            # style. Use isolated registry to avoid global duplication in tests
            self._registry = CollectorRegistry()
            self._c_events = Counter(
                "fapilog_events_processed_total",
                ("Total number of events processed across the pipeline"),
                registry=self._registry,
            )
            self._c_plugin_errors = Counter(
                "fapilog_plugin_errors_total",
                "Total number of plugin execution errors",
                ["plugin"],
                registry=self._registry,
            )
            self._h_process_latency = Histogram(
                "fapilog_event_process_seconds",
                "Latency for processing a single event",
                buckets=(
                    0.0005,
                    0.001,
                    0.0025,
                    0.005,
                    0.01,
                    0.025,
                    0.05,
                    0.1,
                    0.25,
                    0.5,
                    1.0,
                ),
                registry=self._registry,
            )
            self._h_plugin_latency = Histogram(
                "fapilog_plugin_exec_seconds",
                "Latency for executing a single plugin call",
                [
                    "plugin",
                ],
                buckets=(
                    0.0005,
                    0.001,
                    0.0025,
                    0.005,
                    0.01,
                    0.025,
                    0.05,
                    0.1,
                    0.25,
                    0.5,
                    1.0,
                ),
                registry=self._registry,
            )

            # Extended pipeline metrics
            self._c_events_submitted = Counter(
                "fapilog_events_submitted_total",
                "Total number of events submitted to the logger",
                registry=self._registry,
            )
            self._c_events_dropped = Counter(
                "fapilog_events_dropped_total",
                "Total number of events dropped due to backpressure",
                registry=self._registry,
            )
            self._c_events_filtered = Counter(
                "fapilog_events_filtered_total",
                "Total number of events dropped by filters",
                registry=self._registry,
            )
            self._c_backpressure_waits = Counter(
                "fapilog_backpressure_waits_total",
                "Total number of times enqueue waited for capacity",
                registry=self._registry,
            )
            self._h_flush_latency = Histogram(
                "fapilog_flush_seconds",
                "Latency to flush a batch to sinks",
                buckets=(
                    0.0005,
                    0.001,
                    0.0025,
                    0.005,
                    0.01,
                    0.025,
                    0.05,
                    0.1,
                    0.25,
                    0.5,
                    1.0,
                ),
                registry=self._registry,
            )
            self._h_batch_size = Histogram(
                "fapilog_batch_size",
                "Number of events per flush batch",
                buckets=(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024),
                registry=self._registry,
            )
            self._c_sink_errors = Counter(
                "fapilog_sink_errors_total",
                "Total number of sink write errors",
                ["sink"],
                registry=self._registry,
            )
            self._g_queue_high_watermark = Gauge(
                "fapilog_queue_high_watermark",
                "Observed max queue depth since start",
                registry=self._registry,
            )
            self._g_filter_sample_rate = Gauge(
                "fapilog_filter_sample_rate",
                "Current sample rate reported by filters",
                ["filter"],
                registry=self._registry,
            )
            self._g_rate_limit_keys = Gauge(
                "fapilog_rate_limit_keys_tracked",
                "Number of unique rate limit keys currently tracked",
                registry=self._registry,
            )
            self._c_size_guard_truncated = Counter(
                "processor_size_guard_truncated_total",
                "Total number of payloads truncated by size_guard",
                registry=self._registry,
            )
            self._c_size_guard_dropped = Counter(
                "processor_size_guard_dropped_total",
                "Total number of payloads dropped by size_guard",
                registry=self._registry,
            )
            self._c_redaction_exceptions = Counter(
                "fapilog_redaction_exceptions_total",
                "Total number of redaction pipeline exceptions",
                registry=self._registry,
            )

    @property
    def is_enabled(self) -> bool:
        return self._enabled

    @property
    def registry(self) -> CollectorRegistry | None:
        """Expose the isolated Prometheus registry when enabled."""
        return self._registry

    async def record_event_processed(
        self, *, duration_seconds: float | None = None
    ) -> None:
        async with self._lock:
            self._state.events_processed += 1
        if not self._enabled:
            return
        if self._c_events is not None:
            self._c_events.inc()
        if duration_seconds is not None and self._h_process_latency is not None:
            self._h_process_latency.observe(duration_seconds)

    async def record_events_submitted(self, count: int = 1) -> None:
        if not self._enabled:
            return
        if self._c_events_submitted is not None:
            self._c_events_submitted.inc(count)

    async def record_events_dropped(self, count: int = 1) -> None:
        if not self._enabled:
            return
        if self._c_events_dropped is not None:
            self._c_events_dropped.inc(count)

    async def record_events_filtered(self, count: int = 1) -> None:
        async with self._lock:
            self._state.events_filtered += count
        if not self._enabled:
            return
        if self._c_events_filtered is not None:
            self._c_events_filtered.inc(count)

    async def record_backpressure_wait(self, count: int = 1) -> None:
        if not self._enabled:
            return
        if self._c_backpressure_waits is not None:
            self._c_backpressure_waits.inc(count)

    async def record_flush(self, *, batch_size: int, latency_seconds: float) -> None:
        if not self._enabled:
            return
        if self._h_batch_size is not None:
            self._h_batch_size.observe(batch_size)
        if self._h_flush_latency is not None:
            self._h_flush_latency.observe(latency_seconds)

    async def set_queue_high_watermark(self, value: int) -> None:
        if not self._enabled:
            return
        if self._g_queue_high_watermark is not None:
            self._g_queue_high_watermark.set(value)

    async def record_sample_rate(self, filter_name: str, rate: float) -> None:
        if not self._enabled:
            return
        if self._g_filter_sample_rate is not None:
            self._g_filter_sample_rate.labels(filter=filter_name).set(rate)

    async def record_rate_limit_keys_tracked(self, count: int) -> None:
        if not self._enabled:
            return
        if self._g_rate_limit_keys is not None:
            self._g_rate_limit_keys.set(count)

    async def record_size_guard_truncated(self, count: int = 1) -> None:
        async with self._lock:
            self._size_guard_truncated += count
        if not self._enabled:
            return
        if self._c_size_guard_truncated is not None:
            self._c_size_guard_truncated.inc(count)

    async def record_size_guard_dropped(self, count: int = 1) -> None:
        async with self._lock:
            self._size_guard_dropped += count
        if not self._enabled:
            return
        if self._c_size_guard_dropped is not None:
            self._c_size_guard_dropped.inc(count)

    async def record_redaction_exception(self, count: int = 1) -> None:
        """Record redaction pipeline exceptions for monitoring."""
        if not self._enabled:
            return
        if self._c_redaction_exceptions is not None:
            self._c_redaction_exceptions.inc(count)

    async def record_sink_error(
        self, *, sink: str | None = None, count: int = 1
    ) -> None:
        if not self._enabled:
            return
        if self._c_sink_errors is not None:
            self._c_sink_errors.labels(sink=(sink or "unknown")).inc(count)

    async def record_plugin_error(
        self,
        *,
        plugin_name: str | None = None,
    ) -> None:
        async with self._lock:
            self._state.plugin_errors += 1
            if plugin_name:
                stats = self._plugin_stats.setdefault(
                    plugin_name,
                    PluginStats(),
                )
                stats.errors += 1
        if not self._enabled:
            return
        if self._c_plugin_errors is not None:
            label = plugin_name or "unknown"
            self._c_plugin_errors.labels(plugin=label).inc()

    async def snapshot(self) -> PipelineMetrics:
        # Lightweight copy without exposing internals
        async with self._lock:
            events = self._state.events_processed
            filtered = self._state.events_filtered
            errs = self._state.plugin_errors
        profiles = await self.all_plugin_stats()
        # Force-read attributes to satisfy static analysis (vulture) and
        # make aggregate values available to snapshot consumers if desired.
        _ = sum(s.executions for s in profiles.values())
        _ = sum(s.total_duration_seconds for s in profiles.values())
        return PipelineMetrics(
            events_processed=events,
            events_filtered=filtered,
            plugin_errors=errs,
        )

    async def record_plugin_execution(
        self,
        *,
        plugin_name: str,
        duration_seconds: float,
        success: bool = True,
    ) -> None:
        """Record a single plugin execution for profiling.

        Always updates in-memory stats; if exporters are enabled, also updates
        labeled Prometheus metrics.
        """
        async with self._lock:
            stats = self._plugin_stats.setdefault(plugin_name, PluginStats())
            stats.executions += 1
            stats.total_duration_seconds += float(duration_seconds)
            if not success:
                stats.errors += 1

        if not self._enabled:
            return

        if self._h_plugin_latency is not None:
            self._h_plugin_latency.labels(plugin=plugin_name).observe(duration_seconds)

    async def get_plugin_stats(self, plugin_name: str) -> PluginStats:
        async with self._lock:
            return self._plugin_stats.get(plugin_name, PluginStats())

    async def all_plugin_stats(self) -> dict[str, PluginStats]:
        # Build via accessor to mark usage for static analysis and provide
        # consistent copying semantics.
        async with self._lock:
            names = list(self._plugin_stats.keys())
        result: dict[str, PluginStats] = {}
        for name in names:
            result[name] = await self.get_plugin_stats(name)
        return result

    def cleanup(self) -> None:
        """Clear accumulated statistics.

        Called during logger drain to release memory. Safe to call multiple times.
        """
        self._plugin_stats.clear()


class PluginExecutionTimer:
    """Async context manager to profile a single plugin execution.

    On success, records execution duration. On error, records both the error
    and
    the execution duration flagged as a failed attempt. Exceptions are not
    swallowed.
    """

    def __init__(
        self,
        *,
        metrics: MetricsCollector | None,
        plugin_name: str,
    ) -> None:
        self._metrics = metrics
        self._plugin_name = plugin_name
        self._start: float = 0.0

    async def __aenter__(self) -> PluginExecutionTimer:
        self._start = perf_counter()
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: Any,
    ) -> bool:
        if self._metrics is None:
            return False
        duration = perf_counter() - self._start
        # Consume tb to satisfy static analyzers
        _ = tb
        if exc is not None:
            await self._metrics.record_plugin_error(plugin_name=self._plugin_name)
            await self._metrics.record_plugin_execution(
                plugin_name=self._plugin_name,
                duration_seconds=duration,
                success=False,
            )
            return False
        await self._metrics.record_plugin_execution(
            plugin_name=self._plugin_name,
            duration_seconds=duration,
            success=True,
        )
        return False


def plugin_timer(
    metrics: MetricsCollector | None, plugin_name: str
) -> PluginExecutionTimer:
    """Factory for a plugin execution timer context manager."""
    return PluginExecutionTimer(metrics=metrics, plugin_name=plugin_name)
