//! # Prometheus `/metrics` endpoint (deploy-C §5.5)
//!
//! Hand-rolled text exposition on a **separate** axum listener so it never
//! transits the TLS-terminating edge (Caddy). The format is the
//! Prometheus 0.0.4 text line protocol; zero new dependencies.
//!
//! The renderer deliberately skips `rutster_event_sink_drops_total` when
//! the sink reports `None`: emitting "0" would falsely tell an operator
//! the sink is healthy-with-zero-drops, when really no event sink is
//! wired at all. This is a convention call, not a bug — see
//! `render_prometheus_text`.

use axum::Router;
use axum::extract::State;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::get;

use crate::media_thread::MediaStats;
use crate::session_map::AppState;

/// Build the isolated `/metrics` router.
///
/// This router is intentionally NOT merged into the main `routes::router`;
/// `/metrics` must live on its own listener so the edge never forwards it
/// and it stays inside the trust boundary.
pub fn metrics_router(state: AppState) -> Router {
    Router::new()
        .route("/metrics", get(metrics_handler))
        .with_state(state)
}

/// GET /metrics — return a Prometheus text snapshot of the media thread.
///
/// Mirrors `routes::readyz` but with a 1 s timeout (vs readyz's 250 ms).
/// Prometheus's default scrape timeout is 10 s; 1 s is generous while still
/// letting a scraper mark a wedged node as stale.
async fn metrics_handler(State(state): State<AppState>) -> Response {
    let (reply, rx) = tokio::sync::oneshot::channel();
    let stats = match tokio::time::timeout(std::time::Duration::from_secs(1), async {
        state
            .cmd_tx
            .send(crate::media_thread::MediaCmd::Stats { reply })
            .await
            .ok()?;
        rx.await.ok()
    })
    .await
    {
        Ok(Some(s)) => s,
        // Elapsed, send failure, or dropped reply all mean the media thread
        // cannot be sampled right now.
        _ => {
            return (StatusCode::SERVICE_UNAVAILABLE, "media thread unresponsive").into_response();
        }
    };

    let body = render_prometheus_text(&stats);
    (
        StatusCode::OK,
        [(header::CONTENT_TYPE, "text/plain; version=0.0.4")],
        body,
    )
        .into_response()
}

/// Render a [`MediaStats`] snapshot as Prometheus 0.0.4 text.
///
/// Metrics that are conceptually counters get `_total` suffixes; gauges do
/// not. `rutster_event_sink_drops_total` is emitted **only** when the sink
/// reports a concrete drop count — `None` means "no sink present", and
/// emitting "0" would mislead scrapers into believing a sink exists and is
/// healthy.
pub fn render_prometheus_text(stats: &MediaStats) -> String {
    use std::fmt::Write;

    let mut out = String::new();

    let _ = writeln!(
        out,
        "# HELP rutster_sessions_active Current number of active sessions\n# TYPE rutster_sessions_active gauge\nrutster_sessions_active {}\n",
        stats.sessions
    );

    let _ = writeln!(
        out,
        "# HELP rutster_max_sessions Admission capacity for this node\n# TYPE rutster_max_sessions gauge\nrutster_max_sessions {}\n",
        stats.max_sessions
    );

    let _ = writeln!(
        out,
        "# HELP rutster_draining Non-zero when the node is draining and rejecting new sessions\n# TYPE rutster_draining gauge\nrutster_draining {}\n",
        u64::from(stats.draining)
    );

    let _ = writeln!(
        out,
        "# HELP rutster_tick_overruns_total Ticks whose work exceeded the meta-tick budget\n# TYPE rutster_tick_overruns_total counter\nrutster_tick_overruns_total {}\n",
        stats.tick_overruns
    );

    let _ = writeln!(
        out,
        "# HELP rutster_last_tick_micros Wall time of the last meta-tick in microseconds\n# TYPE rutster_last_tick_micros gauge\nrutster_last_tick_micros {}\n",
        stats.last_tick_micros
    );

    let _ = writeln!(
        out,
        "# HELP rutster_admission_rejects_total Sessions rejected due to capacity or drain\n# TYPE rutster_admission_rejects_total counter\nrutster_admission_rejects_total {}\n",
        stats.admission_rejects
    );

    if let Some(drops) = stats.event_sink_drops {
        let _ = writeln!(
            out,
            "# HELP rutster_event_sink_drops_total Lifecycle events dropped by the event sink\n# TYPE rutster_event_sink_drops_total counter\nrutster_event_sink_drops_total {}\n",
            drops
        );
    }

    out
}
