"""Prometheus metrics exposed at `/metrics`.

We keep the surface tiny on purpose:

* `http_requests_total{method,route,status}` — request count
* `http_request_duration_seconds{method,route}` — latency histogram
* `ocr_jobs_total{status}` — current count of jobs in each status, derived
  from the database at scrape time. Because this is computed from the
  ``jobs`` table it is correct regardless of whether the writer was the API
  process (sync mode) or a Celery worker process. Note that this is
  technically a gauge of the cumulative-by-status count, not a strict
  monotonic counter — values can decrease if rows are deleted/reaped.
* `ocr_job_processing_seconds` — pipeline wall-time histogram. Only the API
  process observes events on this histogram (sync path); the worker writes
  its timing into the DB row and is not exposed here.

A custom registry is used so tests can reset counters cleanly.
"""

from __future__ import annotations

import time
from collections.abc import Awaitable, Callable, Iterable

from fastapi import Request, Response
from prometheus_client import (
    CONTENT_TYPE_LATEST,
    CollectorRegistry,
    Counter,
    Histogram,
    generate_latest,
)
from prometheus_client.core import GaugeMetricFamily
from prometheus_client.metrics_core import Metric
from sqlalchemy import func, select
from starlette.middleware.base import BaseHTTPMiddleware

from ocr_sprint.db.base import session_scope
from ocr_sprint.db.models import JobRow
from ocr_sprint.utils.logging import get_logger

_logger = get_logger(__name__)

REGISTRY = CollectorRegistry()

REQUEST_COUNT = Counter(
    "http_requests_total",
    "HTTP requests handled by the API",
    ("method", "route", "status"),
    registry=REGISTRY,
)
REQUEST_LATENCY = Histogram(
    "http_request_duration_seconds",
    "HTTP request latency",
    ("method", "route"),
    registry=REGISTRY,
)
JOB_PROCESSING_SECONDS = Histogram(
    "ocr_job_processing_seconds",
    "OCR job pipeline wall-time as observed by the API process",
    registry=REGISTRY,
)


class _JobStatusCollector:
    """Custom collector that queries the ``jobs`` table on every scrape.

    Because the worker runs in a separate process from the API, an in-memory
    ``Counter`` cannot accurately track terminal job counts — its writes
    would never reach the API's ``/metrics`` endpoint. Reading from the
    shared DB on each scrape keeps the metric correct across processes.
    """

    def collect(self) -> Iterable[Metric]:
        family = GaugeMetricFamily(
            "ocr_jobs_total",
            "Current count of jobs grouped by status (read from DB).",
            labels=["status"],
        )
        try:
            with session_scope() as session:
                stmt = select(JobRow.status, func.count()).group_by(JobRow.status)
                for status_value, count in session.execute(stmt).all():
                    family.add_metric([status_value], float(count))
        except Exception as exc:
            _logger.warning("metrics.jobs_collect_failed", error=str(exc))
        return [family]


REGISTRY.register(_JobStatusCollector())


class MetricsMiddleware(BaseHTTPMiddleware):
    """Record request count + latency. The `route` label is the path
    template, not the raw URL, so per-id endpoints don't blow up cardinality.
    """

    async def dispatch(
        self,
        request: Request,
        call_next: Callable[[Request], Awaitable[Response]],
    ) -> Response:
        start = time.perf_counter()
        response = await call_next(request)
        elapsed = time.perf_counter() - start
        route = request.scope.get("route")
        path = getattr(route, "path", request.url.path) if route else request.url.path
        REQUEST_COUNT.labels(request.method, path, str(response.status_code)).inc()
        REQUEST_LATENCY.labels(request.method, path).observe(elapsed)
        return response


async def metrics_endpoint() -> Response:
    return Response(content=generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST)
