"""Metrics interface for SDK observability.

Implementations bridge to Prometheus, OTel, or custom backends.
The SDK ships with NoopMetrics (zero overhead) to avoid forcing
a metrics dependency.
"""

from __future__ import annotations

from typing import Protocol


class Metrics(Protocol):
    """Protocol defining the metrics contract for the NeonLink SDK."""

    def publish_success(self, topic: str, latency_sec: float) -> None: ...
    def publish_error(self, topic: str, err: Exception) -> None: ...
    def consume_success(self, topic: str, partition: int, latency_sec: float) -> None: ...
    def consume_error(self, topic: str, partition: int, err: Exception) -> None: ...
    def poison_pill_detected(self, topic: str, partition: int, offset: int) -> None: ...
    def poison_pill_blocked(self, topic: str, partition: int, offset: int) -> None: ...
    def consumer_lag(self, topic: str, partition: int, lag: int) -> None:
        """Report per-fetch processing lag (high watermark - current offset).

        This is NOT committed-offset group lag. For true consumer group lag,
        use Kafka admin APIs externally.
        """
        ...
    def circuit_breaker_state_change(self, name: str, from_state: str, to_state: str) -> None: ...
    def dlq_routed(self, original_topic: str, err: Exception) -> None: ...


class NoopMetrics:
    """Default metrics implementation with zero overhead. All methods are no-ops."""

    def publish_success(self, topic: str, latency_sec: float) -> None:
        pass

    def publish_error(self, topic: str, err: Exception) -> None:
        pass

    def consume_success(self, topic: str, partition: int, latency_sec: float) -> None:
        pass

    def consume_error(self, topic: str, partition: int, err: Exception) -> None:
        pass

    def poison_pill_detected(self, topic: str, partition: int, offset: int) -> None:
        pass

    def poison_pill_blocked(self, topic: str, partition: int, offset: int) -> None:
        pass

    def consumer_lag(self, topic: str, partition: int, lag: int) -> None:
        pass

    def circuit_breaker_state_change(self, name: str, from_state: str, to_state: str) -> None:
        pass

    def dlq_routed(self, original_topic: str, err: Exception) -> None:
        pass


# Circuit breaker state string to numeric gauge value.
_CB_STATE_MAP = {"closed": 0, "half_open": 1, "open": 2}


class OTelMetrics:
    """Metrics implementation backed by OpenTelemetry SDK instruments.

    Creates counters, histograms, and gauges named per the observability
    doctrine (Section 6.3)::

        mcfo_{service_name}_broker_{metric}_{unit}

    Requires ``opentelemetry-api`` to be installed (the ``tracing`` or
    ``observability`` extras include it). If OTel is not available, use
    ``NoopMetrics`` instead.

    Usage::

        from opentelemetry.metrics import get_meter
        meter = get_meter("myservice")
        metrics = OTelMetrics("findata", meter=meter)
        cfg = neonlink.Config(metrics=metrics)

    Histogram bucket boundaries follow doctrine Section 6.5. The Python
    OTel SDK configures buckets via Views on the MeterProvider, not at
    instrument creation. Recommended view config for consuming services::

        from opentelemetry.sdk.metrics import MeterProvider
        from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation

        views = [
            View(
                instrument_name="mcfo_*_broker_publish_duration_seconds",
                aggregation=ExplicitBucketHistogramAggregation(
                    boundaries=[.001, .005, .01, .025, .05, .1, .25, .5, 1]
                ),
            ),
            View(
                instrument_name="mcfo_*_broker_consume_duration_seconds",
                aggregation=ExplicitBucketHistogramAggregation(
                    boundaries=[.01, .05, .1, .25, .5, 1, 2.5, 5, 10, 30, 60]
                ),
            ),
        ]
        provider = MeterProvider(views=views, ...)
    """

    def __init__(self, service_name: str, *, meter=None) -> None:
        try:
            from opentelemetry import metrics as otel_metrics
        except ImportError as exc:
            raise ImportError(
                "opentelemetry-api is required for OTelMetrics. "
                "Install it with: pip install neonlink-client[tracing]"
            ) from exc

        if meter is None:
            meter = otel_metrics.get_meter("neonlink")

        prefix = f"mcfo_{service_name}_broker"

        self._publish_duration = meter.create_histogram(
            f"{prefix}_publish_duration_seconds",
            description="Broker publish latency in seconds",
            unit="s",
        )
        self._publish_errors = meter.create_counter(
            f"{prefix}_publish_errors_total",
            description="Total broker publish errors",
        )
        self._consume_duration = meter.create_histogram(
            f"{prefix}_consume_duration_seconds",
            description="Message processing latency in seconds",
            unit="s",
        )
        self._consume_errors = meter.create_counter(
            f"{prefix}_consume_errors_total",
            description="Total message consume errors",
        )
        self._poison_detected = meter.create_counter(
            f"{prefix}_poison_pill_detected_total",
            description="Total poison pill detections",
        )
        self._poison_blocked = meter.create_counter(
            f"{prefix}_poison_pill_blocked_total",
            description="Total poison pills blocked (no DLQ)",
        )
        self._consumer_lag = meter.create_gauge(
            f"{prefix}_consumer_lag",
            description="Per-partition consumer lag (high watermark - current offset)",
        )
        self._cb_state = meter.create_gauge(
            f"{prefix}_circuit_breaker_state",
            description="Circuit breaker state (0=closed, 1=half_open, 2=open)",
        )
        self._dlq_routed = meter.create_counter(
            f"{prefix}_dlq_routed_total",
            description="Total records routed to dead letter queue",
        )

    def publish_success(self, topic: str, latency_sec: float) -> None:
        self._publish_duration.record(latency_sec, {"topic": topic})

    def publish_error(self, topic: str, err: Exception) -> None:
        self._publish_errors.add(1, {"topic": topic})

    def consume_success(self, topic: str, partition: int, latency_sec: float) -> None:
        self._consume_duration.record(
            latency_sec, {"topic": topic, "partition": partition}
        )

    def consume_error(self, topic: str, partition: int, err: Exception) -> None:
        self._consume_errors.add(1, {"topic": topic, "partition": partition})

    def poison_pill_detected(self, topic: str, partition: int, offset: int) -> None:
        self._poison_detected.add(1, {"topic": topic, "partition": partition})

    def poison_pill_blocked(self, topic: str, partition: int, offset: int) -> None:
        self._poison_blocked.add(1, {"topic": topic, "partition": partition})

    def consumer_lag(self, topic: str, partition: int, lag: int) -> None:
        self._consumer_lag.set(lag, {"topic": topic, "partition": partition})

    def circuit_breaker_state_change(self, name: str, from_state: str, to_state: str) -> None:
        self._cb_state.set(_CB_STATE_MAP.get(to_state, -1), {"name": name})

    def dlq_routed(self, original_topic: str, err: Exception) -> None:
        self._dlq_routed.add(1, {"topic": original_topic})
