"""Prometheus metrics endpoint and central registry.

Exposes operational metrics via ``GET /api/metrics`` in the standard
Prometheus text format. Unauthenticated by design — Prometheus scrapers do
not authenticate. If the API port crosses a trust boundary, disable via
``NOTIFY_BRIDGE_METRICS_ENABLED=false``.

Metrics are defined as module-level singletons so the rest of the codebase
can ``from notify_bridge_server.api.metrics import metrics`` and call
``metrics.dispatch_duration.labels(channel="telegram").observe(0.42)``
without re-creating the underlying objects.

Other modules MUST NOT ``import prometheus_client`` directly. Route every
metric through :data:`metrics` (a :class:`MetricsRegistry`) so we have one
place to swap implementations or add labels.
"""

from __future__ import annotations

import logging
from typing import Final

from fastapi import APIRouter, HTTPException
from starlette.responses import Response

from prometheus_client import (
    CONTENT_TYPE_LATEST,
    CollectorRegistry,
    Counter,
    Gauge,
    Histogram,
    generate_latest,
)

from ..config import settings as _settings

_LOGGER = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Metric definitions
# ---------------------------------------------------------------------------
# Use a dedicated CollectorRegistry instead of the global default registry so
# tests can construct the module repeatedly without ``Duplicated timeseries``
# errors and so we never accidentally export Python GC / process metrics that
# aren't part of the documented surface in OPERATIONS.md.

_REGISTRY: Final[CollectorRegistry] = CollectorRegistry()


class MetricsRegistry:
    """Singleton holder for module-level Prometheus collectors.

    Instantiated once at import time as :data:`metrics`. Keep collectors as
    instance attributes so call sites get IDE autocomplete and so swapping
    the collector type (e.g. Counter -> Summary) is a one-line change here.
    """

    def __init__(self, registry: CollectorRegistry) -> None:
        self.registry = registry

        # Gauge: populated on every scrape via the collector hook below.
        self.deferred_pending = Gauge(
            "notify_bridge_deferred_pending",
            "Count of deferred_dispatch rows awaiting drain.",
            registry=registry,
        )

        # Counter: incremented after each event_log row is persisted.
        self.event_log_total = Counter(
            "notify_bridge_event_log_total",
            "Total events written to event_log, partitioned by status and event_type.",
            ["status", "event_type"],
            registry=registry,
        )

        # Histogram: observed wall-clock seconds per outbound dispatch attempt.
        self.dispatch_duration = Histogram(
            "notify_bridge_dispatch_duration_seconds",
            "Wall-clock duration of one dispatch attempt to a notification channel.",
            ["channel"],
            registry=registry,
            buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
        )

        # Counter: each polling provider that fails a tick increments by 1.
        self.provider_poll_failures = Counter(
            "notify_bridge_provider_poll_failures_total",
            "Polling provider failures partitioned by provider type.",
            ["provider_type"],
            registry=registry,
        )

        # Counter: each rejected delivery to a target increments by 1.
        self.target_send_failures = Counter(
            "notify_bridge_target_send_failures_total",
            "Failed sends to a target partitioned by target type and HTTP status.",
            ["target_type", "status_code"],
            registry=registry,
        )


metrics: Final[MetricsRegistry] = MetricsRegistry(_REGISTRY)


# ---------------------------------------------------------------------------
# Scrape hook: refresh dynamic gauges on demand
# ---------------------------------------------------------------------------

async def _refresh_deferred_pending_gauge() -> None:
    """Populate ``deferred_pending`` by counting pending rows in the DB.

    Called from the request handler before serializing — we don't poll the
    DB on a fixed cadence to avoid a steady-state cost when nothing is
    scraping. Kept tolerant: a DB error logs and leaves the previous value.
    """
    try:
        from sqlalchemy import text

        from ..database.engine import get_engine

        engine = get_engine()
        async with engine.connect() as conn:
            result = await conn.execute(
                text("SELECT count(*) FROM deferred_dispatch WHERE status='pending'")
            )
            row = result.first()
            count = int(row[0]) if row else 0
        metrics.deferred_pending.set(count)
    except Exception as exc:  # noqa: BLE001 — never fail the scrape over this
        _LOGGER.debug("deferred_pending refresh skipped: %s", exc)


# ---------------------------------------------------------------------------
# Router
# ---------------------------------------------------------------------------

router = APIRouter(tags=["metrics"])


@router.get("/api/metrics")
async def metrics_endpoint() -> Response:
    """Expose collected metrics in Prometheus text format.

    No auth by design — Prometheus scrapers don't authenticate. Gate the
    endpoint via ``NOTIFY_BRIDGE_METRICS_ENABLED=false`` when the API port
    is reachable from outside the trust boundary.
    """
    if not _settings.metrics_enabled:
        raise HTTPException(status_code=404, detail="Metrics disabled")

    await _refresh_deferred_pending_gauge()

    # Stub increments so the endpoint reports non-empty data even before
    # callers wire instrumentation. Removed once code-paths are instrumented.
    # The labels here intentionally use a sentinel value so dashboards can
    # filter the noise out: ``status="bootstrap"``.
    metrics.event_log_total.labels(status="bootstrap", event_type="metrics_scrape").inc(0)

    payload = generate_latest(_REGISTRY)
    return Response(content=payload, media_type=CONTENT_TYPE_LATEST)
