//! Observability primitives for Athena Auth runtimes.
//!
//! The crate owns the Prometheus registry and the lightweight HTML metrics
//! dashboard so server entrypoints can wire observability without rebuilding
//! metric collectors inline.

use std::collections::BTreeMap;
use std::fmt;
use std::sync::{Mutex, MutexGuard};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use prometheus::{
    Encoder, HistogramOpts, HistogramVec, IntCounterVec, IntGauge, IntGaugeVec, Registry,
    TextEncoder, opts,
};

/// Error returned while building or rendering observability state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetricsError {
    message: String,
}

impl MetricsError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}

impl fmt::Display for MetricsError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.message)
    }
}

impl std::error::Error for MetricsError {}

/// Runtime configuration for HTTP metrics.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpMetricsConfig {
    /// Optional Prometheus namespace prefix, for example `athena_auth`.
    pub prefix: Option<String>,
    /// Service label exposed on the build-info metric and dashboard.
    pub service_name: String,
    /// Version label exposed on the build-info metric and dashboard.
    pub service_version: String,
}

impl Default for HttpMetricsConfig {
    fn default() -> Self {
        Self {
            prefix: None,
            service_name: "athena-auth".to_string(),
            service_version: "unknown".to_string(),
        }
    }
}

/// A completed HTTP request observation.
#[derive(Debug, Clone, PartialEq)]
pub struct HttpRequestObservation {
    method: String,
    route: String,
    status: u16,
    duration: Duration,
    request_body_bytes: Option<u64>,
    response_body_bytes: Option<u64>,
}

impl HttpRequestObservation {
    /// Creates a request observation with no body-size measurements.
    pub fn new(
        method: impl Into<String>,
        route: impl Into<String>,
        status: u16,
        duration: Duration,
    ) -> Self {
        Self {
            method: method.into(),
            route: route.into(),
            status,
            duration,
            request_body_bytes: None,
            response_body_bytes: None,
        }
    }

    /// Adds the request body size when it is known.
    pub fn with_request_body_bytes(mut self, request_body_bytes: Option<u64>) -> Self {
        self.request_body_bytes = request_body_bytes;
        self
    }

    /// Adds the response body size when it is known.
    pub fn with_response_body_bytes(mut self, response_body_bytes: Option<u64>) -> Self {
        self.response_body_bytes = response_body_bytes;
        self
    }
}

/// Point-in-time metrics snapshot used by the HTML dashboard.
#[derive(Debug, Clone, PartialEq)]
pub struct MetricsSnapshot {
    pub service_name: String,
    pub service_version: String,
    pub total_requests: u64,
    pub in_flight_requests: u64,
    pub total_errors: u64,
    pub client_errors: u64,
    pub server_errors: u64,
    pub average_latency_ms: f64,
    pub max_latency_ms: f64,
    pub last_status: Option<u16>,
    pub process_start_time_unix_seconds: u64,
    pub routes: Vec<RouteMetricsSnapshot>,
}

/// Per-route aggregate used by the HTML dashboard.
#[derive(Debug, Clone, PartialEq)]
pub struct RouteMetricsSnapshot {
    pub method: String,
    pub route: String,
    pub status: u16,
    pub status_class: &'static str,
    pub request_count: u64,
    pub error_count: u64,
    pub average_latency_ms: f64,
    pub max_latency_ms: f64,
    pub last_latency_ms: f64,
    pub last_seen_unix_seconds: u64,
}

/// HTTP metrics registry plus a small in-memory summary for human inspection.
#[derive(Debug)]
pub struct HttpMetrics {
    registry: Registry,
    requests_total: IntCounterVec,
    request_duration_seconds: HistogramVec,
    requests_in_flight: IntGaugeVec,
    request_errors_total: IntCounterVec,
    responses_total: IntCounterVec,
    request_body_bytes: HistogramVec,
    response_body_bytes: HistogramVec,
    service_name: String,
    service_version: String,
    process_start_time_unix_seconds: u64,
    summary: Mutex<MetricsSummary>,
}

impl HttpMetrics {
    /// Creates metrics with the provided optional Prometheus prefix.
    pub fn new(prefix: Option<&str>) -> Result<Self, MetricsError> {
        Self::from_config(HttpMetricsConfig {
            prefix: prefix.map(str::to_string),
            ..HttpMetricsConfig::default()
        })
    }

    /// Creates metrics with full service metadata.
    pub fn from_config(config: HttpMetricsConfig) -> Result<Self, MetricsError> {
        let registry = match config
            .prefix
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
        {
            Some(metric_prefix) => Registry::new_custom(Some(metric_prefix.to_string()), None)
                .map_err(|error| {
                    MetricsError::new(format!("invalid Prometheus metric prefix: {error}"))
                })?,
            None => Registry::new(),
        };

        let requests_total = IntCounterVec::new(
            opts!(
                "http_requests_total",
                "Total number of HTTP requests handled by the server"
            ),
            &["method", "route", "status"],
        )
        .map_err(|error| {
            MetricsError::new(format!("failed to create Prometheus counter: {error}"))
        })?;

        let request_duration_seconds = HistogramVec::new(
            HistogramOpts::new(
                "http_request_duration_seconds",
                "HTTP request latency in seconds",
            )
            .buckets(vec![
                0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
            ]),
            &["method", "route", "status"],
        )
        .map_err(|error| {
            MetricsError::new(format!("failed to create Prometheus histogram: {error}"))
        })?;

        let requests_in_flight = IntGaugeVec::new(
            opts!(
                "http_requests_in_flight",
                "Current number of HTTP requests being handled by the server"
            ),
            &["method", "route"],
        )
        .map_err(|error| {
            MetricsError::new(format!(
                "failed to create Prometheus in-flight gauge: {error}"
            ))
        })?;

        let request_errors_total = IntCounterVec::new(
            opts!(
                "http_request_errors_total",
                "Total number of HTTP requests completed with a 4xx or 5xx response"
            ),
            &["method", "route", "status"],
        )
        .map_err(|error| {
            MetricsError::new(format!(
                "failed to create Prometheus error counter: {error}"
            ))
        })?;

        let responses_total = IntCounterVec::new(
            opts!(
                "http_responses_total",
                "Total number of HTTP responses grouped by status class"
            ),
            &["status_class"],
        )
        .map_err(|error| {
            MetricsError::new(format!(
                "failed to create Prometheus response counter: {error}"
            ))
        })?;

        let request_body_bytes = HistogramVec::new(
            HistogramOpts::new(
                "http_request_body_bytes",
                "HTTP request body size in bytes when Content-Length is known",
            )
            .buckets(vec![
                0.0, 128.0, 512.0, 1024.0, 4096.0, 16384.0, 65536.0, 262144.0, 1048576.0, 5242880.0,
            ]),
            &["method", "route", "status"],
        )
        .map_err(|error| {
            MetricsError::new(format!(
                "failed to create Prometheus request body histogram: {error}"
            ))
        })?;

        let response_body_bytes = HistogramVec::new(
            HistogramOpts::new(
                "http_response_body_bytes",
                "HTTP response body size in bytes when Content-Length is known",
            )
            .buckets(vec![
                0.0, 128.0, 512.0, 1024.0, 4096.0, 16384.0, 65536.0, 262144.0, 1048576.0, 5242880.0,
            ]),
            &["method", "route", "status"],
        )
        .map_err(|error| {
            MetricsError::new(format!(
                "failed to create Prometheus response body histogram: {error}"
            ))
        })?;

        let process_start_time_seconds = IntGauge::new(
            "process_start_time_seconds",
            "Unix timestamp in seconds when this Athena Auth process started",
        )
        .map_err(|error| {
            MetricsError::new(format!(
                "failed to create Prometheus process start gauge: {error}"
            ))
        })?;

        let build_info = IntGaugeVec::new(
            opts!(
                "build_info",
                "Athena Auth service metadata exposed as a constant gauge"
            ),
            &["service", "version"],
        )
        .map_err(|error| {
            MetricsError::new(format!(
                "failed to create Prometheus build info gauge: {error}"
            ))
        })?;

        register(&registry, requests_total.clone(), "request counter")?;
        register(
            &registry,
            request_duration_seconds.clone(),
            "request duration histogram",
        )?;
        register(
            &registry,
            requests_in_flight.clone(),
            "in-flight request gauge",
        )?;
        register(
            &registry,
            request_errors_total.clone(),
            "request error counter",
        )?;
        register(&registry, responses_total.clone(), "response counter")?;
        register(
            &registry,
            request_body_bytes.clone(),
            "request body histogram",
        )?;
        register(
            &registry,
            response_body_bytes.clone(),
            "response body histogram",
        )?;
        register(
            &registry,
            process_start_time_seconds.clone(),
            "process start gauge",
        )?;
        register(&registry, build_info.clone(), "build info gauge")?;

        let process_start_time_unix_seconds = unix_seconds_now();
        process_start_time_seconds.set(saturating_i64(process_start_time_unix_seconds));
        build_info
            .with_label_values(&[
                config.service_name.as_str(),
                config.service_version.as_str(),
            ])
            .set(1);

        Ok(Self {
            registry,
            requests_total,
            request_duration_seconds,
            requests_in_flight,
            request_errors_total,
            responses_total,
            request_body_bytes,
            response_body_bytes,
            service_name: config.service_name,
            service_version: config.service_version,
            process_start_time_unix_seconds,
            summary: Mutex::new(MetricsSummary::default()),
        })
    }

    /// Records that a request started so in-flight gauges are useful.
    pub fn start_request(&self, method: &str, route: &str) {
        self.requests_in_flight
            .with_label_values(&[method, route])
            .inc();
        let mut summary = self.summary();
        summary.in_flight_requests = summary.in_flight_requests.saturating_add(1);
    }

    /// Records a completed request and updates all Prometheus collectors.
    pub fn finish_request(&self, observation: HttpRequestObservation) {
        let method = observation.method.as_str();
        let route = observation.route.as_str();
        let status_label = observation.status.to_string();
        let status_class = status_class(observation.status);
        let duration_seconds = observation.duration.as_secs_f64();

        self.requests_in_flight
            .with_label_values(&[method, route])
            .dec();
        self.requests_total
            .with_label_values(&[method, route, status_label.as_str()])
            .inc();
        self.request_duration_seconds
            .with_label_values(&[method, route, status_label.as_str()])
            .observe(duration_seconds);
        self.responses_total
            .with_label_values(&[status_class])
            .inc();

        if is_error_status(observation.status) {
            self.request_errors_total
                .with_label_values(&[method, route, status_label.as_str()])
                .inc();
        }

        if let Some(bytes) = observation.request_body_bytes {
            self.request_body_bytes
                .with_label_values(&[method, route, status_label.as_str()])
                .observe(bytes as f64);
        }

        if let Some(bytes) = observation.response_body_bytes {
            self.response_body_bytes
                .with_label_values(&[method, route, status_label.as_str()])
                .observe(bytes as f64);
        }

        let mut summary = self.summary();
        summary.in_flight_requests = summary.in_flight_requests.saturating_sub(1);
        summary.total_requests = summary.total_requests.saturating_add(1);
        summary.total_latency_seconds += duration_seconds;
        summary.max_latency_seconds = summary.max_latency_seconds.max(duration_seconds);
        summary.last_status = Some(observation.status);

        if observation.status >= 400 && observation.status < 500 {
            summary.client_errors = summary.client_errors.saturating_add(1);
        } else if observation.status >= 500 {
            summary.server_errors = summary.server_errors.saturating_add(1);
        }

        let key = RouteKey {
            method: observation.method,
            route: observation.route,
            status: observation.status,
        };
        let route_stats = summary.routes.entry(key).or_default();
        route_stats.request_count = route_stats.request_count.saturating_add(1);
        route_stats.total_latency_seconds += duration_seconds;
        route_stats.max_latency_seconds = route_stats.max_latency_seconds.max(duration_seconds);
        route_stats.last_latency_seconds = duration_seconds;
        route_stats.last_seen_unix_seconds = unix_seconds_now();
        if is_error_status(observation.status) {
            route_stats.error_count = route_stats.error_count.saturating_add(1);
        }
    }

    /// Compatibility helper for callers that only need to record a completed request.
    pub fn observe(&self, method: &str, route: &str, status: &str, duration_seconds: f64) {
        let status_code = status.parse::<u16>().unwrap_or(0);
        let duration = if duration_seconds.is_finite() && duration_seconds >= 0.0 {
            Duration::from_secs_f64(duration_seconds)
        } else {
            Duration::from_secs(0)
        };
        self.start_request(method, route);
        self.finish_request(HttpRequestObservation::new(
            method.to_string(),
            route.to_string(),
            status_code,
            duration,
        ));
    }

    /// Encodes all registered collectors in Prometheus text exposition format.
    pub fn encode(&self) -> Result<(String, String), MetricsError> {
        let mut buffer = Vec::new();
        let encoder = TextEncoder::new();
        let metrics = self.registry.gather();
        encoder.encode(&metrics, &mut buffer).map_err(|error| {
            MetricsError::new(format!("failed to encode Prometheus metrics: {error}"))
        })?;
        let body = String::from_utf8(buffer).map_err(|error| {
            MetricsError::new(format!("Prometheus metrics were not valid UTF-8: {error}"))
        })?;
        Ok((body, encoder.format_type().to_string()))
    }

    /// Returns a stable current summary for operator-facing pages.
    pub fn snapshot(&self) -> MetricsSnapshot {
        let summary = self.summary();
        let average_latency_ms = average_ms(summary.total_latency_seconds, summary.total_requests);
        let mut routes: Vec<RouteMetricsSnapshot> = summary
            .routes
            .iter()
            .map(|(key, stats)| RouteMetricsSnapshot {
                method: key.method.clone(),
                route: key.route.clone(),
                status: key.status,
                status_class: status_class(key.status),
                request_count: stats.request_count,
                error_count: stats.error_count,
                average_latency_ms: average_ms(stats.total_latency_seconds, stats.request_count),
                max_latency_ms: stats.max_latency_seconds * 1000.0,
                last_latency_ms: stats.last_latency_seconds * 1000.0,
                last_seen_unix_seconds: stats.last_seen_unix_seconds,
            })
            .collect();
        routes.sort_by(|left, right| {
            right
                .request_count
                .cmp(&left.request_count)
                .then_with(|| right.error_count.cmp(&left.error_count))
                .then_with(|| left.route.cmp(&right.route))
                .then_with(|| left.method.cmp(&right.method))
                .then_with(|| left.status.cmp(&right.status))
        });

        MetricsSnapshot {
            service_name: self.service_name.clone(),
            service_version: self.service_version.clone(),
            total_requests: summary.total_requests,
            in_flight_requests: summary.in_flight_requests,
            total_errors: summary.client_errors.saturating_add(summary.server_errors),
            client_errors: summary.client_errors,
            server_errors: summary.server_errors,
            average_latency_ms,
            max_latency_ms: summary.max_latency_seconds * 1000.0,
            last_status: summary.last_status,
            process_start_time_unix_seconds: self.process_start_time_unix_seconds,
            routes,
        }
    }

    /// Renders a compact self-contained HTML metrics dashboard.
    pub fn render_dashboard(&self, prometheus_path: &str) -> String {
        let snapshot = self.snapshot();
        let escaped_service = escape_html(snapshot.service_name.as_str());
        let escaped_version = escape_html(snapshot.service_version.as_str());
        let escaped_prometheus_path = escape_html(prometheus_path);
        let error_rate = percentage(snapshot.total_errors, snapshot.total_requests);

        let mut rows = String::new();
        for route in snapshot.routes.iter().take(40) {
            rows.push_str("<tr>");
            rows.push_str("<td><code>");
            rows.push_str(escape_html(route.method.as_str()).as_str());
            rows.push_str("</code></td><td><code>");
            rows.push_str(escape_html(route.route.as_str()).as_str());
            rows.push_str("</code></td><td>");
            rows.push_str(route.status.to_string().as_str());
            rows.push_str("</td><td>");
            rows.push_str(escape_html(route.status_class).as_str());
            rows.push_str("</td><td>");
            rows.push_str(route.request_count.to_string().as_str());
            rows.push_str("</td><td>");
            rows.push_str(route.error_count.to_string().as_str());
            rows.push_str("</td><td>");
            rows.push_str(format_ms(route.average_latency_ms).as_str());
            rows.push_str("</td><td>");
            rows.push_str(format_ms(route.max_latency_ms).as_str());
            rows.push_str("</td><td>");
            rows.push_str(route.last_seen_unix_seconds.to_string().as_str());
            rows.push_str("</td></tr>");
        }

        if rows.is_empty() {
            rows.push_str(
                "<tr><td colspan=\"9\" class=\"empty\">No requests observed since startup.</td></tr>",
            );
        }

        format!(
            r#"<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="10">
  <title>{escaped_service} metrics</title>
  <style>
    :root {{
      color-scheme: light;
      --bg: #ffffff;
      --text: #111827;
      --muted: #4b5563;
      --line: #d1d5db;
      --panel: #f9fafb;
      --blue: #1d4ed8;
      --amber: #b45309;
      --red: #b91c1c;
      --green: #047857;
    }}
    * {{ box-sizing: border-box; }}
    body {{
      margin: 0;
      background: var(--bg);
      color: var(--text);
      font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    }}
    main {{
      width: min(1180px, calc(100vw - 32px));
      margin: 0 auto;
      padding: 28px 0 40px;
    }}
    header {{
      display: flex;
      justify-content: space-between;
      align-items: flex-end;
      gap: 16px;
      padding-bottom: 18px;
      border-bottom: 1px solid var(--line);
    }}
    h1 {{
      margin: 0;
      font-size: 26px;
      letter-spacing: 0;
    }}
    .subtle {{ color: var(--muted); margin-top: 4px; }}
    .link {{
      color: var(--blue);
      text-decoration: none;
      font-weight: 650;
    }}
    .grid {{
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
      gap: 12px;
      margin: 20px 0;
    }}
    .metric {{
      border: 1px solid var(--line);
      background: var(--panel);
      border-radius: 8px;
      padding: 14px;
      min-height: 88px;
    }}
    .metric span {{
      color: var(--muted);
      display: block;
      font-size: 12px;
      text-transform: uppercase;
      letter-spacing: .04em;
    }}
    .metric strong {{
      display: block;
      margin-top: 8px;
      font-size: 24px;
      letter-spacing: 0;
    }}
    .ok strong {{ color: var(--green); }}
    .warn strong {{ color: var(--amber); }}
    .bad strong {{ color: var(--red); }}
    section {{
      margin-top: 24px;
    }}
    h2 {{
      margin: 0 0 10px;
      font-size: 17px;
      letter-spacing: 0;
    }}
    .table-wrap {{
      border: 1px solid var(--line);
      border-radius: 8px;
      overflow: auto;
    }}
    table {{
      border-collapse: collapse;
      width: 100%;
      min-width: 880px;
      background: #ffffff;
    }}
    th, td {{
      border-bottom: 1px solid var(--line);
      padding: 9px 10px;
      text-align: left;
      vertical-align: top;
      white-space: nowrap;
    }}
    th {{
      background: var(--panel);
      color: #111827;
      font-size: 12px;
      text-transform: uppercase;
      letter-spacing: .04em;
    }}
    tr:last-child td {{ border-bottom: 0; }}
    code {{
      color: #111827;
      background: #eef2ff;
      border: 1px solid #c7d2fe;
      border-radius: 4px;
      padding: 2px 4px;
    }}
    .empty {{
      color: var(--muted);
      text-align: center;
      padding: 24px;
    }}
    @media (max-width: 680px) {{
      main {{ width: min(100vw - 20px, 1180px); padding-top: 18px; }}
      header {{ align-items: flex-start; flex-direction: column; }}
      h1 {{ font-size: 22px; }}
    }}
  </style>
</head>
<body>
  <main>
    <header>
      <div>
        <h1>{escaped_service} metrics</h1>
        <div class="subtle">Version {escaped_version} &middot; process started at Unix second {process_start}</div>
      </div>
      <a class="link" href="{escaped_prometheus_path}">Prometheus scrape output</a>
    </header>

    <div class="grid">
      <div class="metric ok"><span>Total requests</span><strong>{total_requests}</strong></div>
      <div class="metric"><span>In flight</span><strong>{in_flight}</strong></div>
      <div class="metric {error_class}"><span>Error rate</span><strong>{error_rate}</strong></div>
      <div class="metric warn"><span>4xx responses</span><strong>{client_errors}</strong></div>
      <div class="metric bad"><span>5xx responses</span><strong>{server_errors}</strong></div>
      <div class="metric"><span>Avg latency</span><strong>{average_latency}</strong></div>
      <div class="metric"><span>Max latency</span><strong>{max_latency}</strong></div>
      <div class="metric"><span>Last status</span><strong>{last_status}</strong></div>
    </div>

    <section>
      <h2>Routes</h2>
      <div class="table-wrap">
        <table>
          <thead>
            <tr>
              <th>Method</th>
              <th>Route</th>
              <th>Status</th>
              <th>Class</th>
              <th>Requests</th>
              <th>Errors</th>
              <th>Avg</th>
              <th>Max</th>
              <th>Last Seen</th>
            </tr>
          </thead>
          <tbody>{rows}</tbody>
        </table>
      </div>
    </section>
  </main>
</body>
</html>"#,
            process_start = snapshot.process_start_time_unix_seconds,
            total_requests = snapshot.total_requests,
            in_flight = snapshot.in_flight_requests,
            error_class = if snapshot.total_errors > 0 {
                "bad"
            } else {
                "ok"
            },
            error_rate = error_rate,
            client_errors = snapshot.client_errors,
            server_errors = snapshot.server_errors,
            average_latency = format_ms(snapshot.average_latency_ms),
            max_latency = format_ms(snapshot.max_latency_ms),
            last_status = snapshot
                .last_status
                .map(|value| value.to_string())
                .unwrap_or_else(|| "-".to_string()),
        )
    }

    fn summary(&self) -> MutexGuard<'_, MetricsSummary> {
        match self.summary.lock() {
            Ok(guard) => guard,
            Err(poisoned) => poisoned.into_inner(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct RouteKey {
    method: String,
    route: String,
    status: u16,
}

#[derive(Debug, Default, Clone, PartialEq)]
struct RouteStats {
    request_count: u64,
    error_count: u64,
    total_latency_seconds: f64,
    max_latency_seconds: f64,
    last_latency_seconds: f64,
    last_seen_unix_seconds: u64,
}

#[derive(Debug, Default, Clone, PartialEq)]
struct MetricsSummary {
    total_requests: u64,
    in_flight_requests: u64,
    client_errors: u64,
    server_errors: u64,
    total_latency_seconds: f64,
    max_latency_seconds: f64,
    last_status: Option<u16>,
    routes: BTreeMap<RouteKey, RouteStats>,
}

fn register<C>(registry: &Registry, collector: C, name: &str) -> Result<(), MetricsError>
where
    C: prometheus::core::Collector + Clone + 'static,
{
    registry.register(Box::new(collector)).map_err(|error| {
        MetricsError::new(format!("failed to register Prometheus {name}: {error}"))
    })
}

fn status_class(status: u16) -> &'static str {
    match status {
        100..=199 => "1xx",
        200..=299 => "2xx",
        300..=399 => "3xx",
        400..=499 => "4xx",
        500..=599 => "5xx",
        _ => "other",
    }
}

fn is_error_status(status: u16) -> bool {
    status >= 400
}

fn average_ms(total_seconds: f64, count: u64) -> f64 {
    if count == 0 {
        0.0
    } else {
        (total_seconds / count as f64) * 1000.0
    }
}

fn format_ms(value: f64) -> String {
    if value >= 1000.0 {
        format!("{:.2}s", value / 1000.0)
    } else {
        format!("{value:.1}ms")
    }
}

fn percentage(numerator: u64, denominator: u64) -> String {
    if denominator == 0 {
        "0.0%".to_string()
    } else {
        format!("{:.1}%", (numerator as f64 / denominator as f64) * 100.0)
    }
}

fn unix_seconds_now() -> u64 {
    match SystemTime::now().duration_since(UNIX_EPOCH) {
        Ok(duration) => duration.as_secs(),
        Err(_) => 0,
    }
}

fn saturating_i64(value: u64) -> i64 {
    i64::try_from(value).unwrap_or(i64::MAX)
}

fn escape_html(value: &str) -> String {
    let mut escaped = String::with_capacity(value.len());
    for character in value.chars() {
        match character {
            '&' => escaped.push_str("&amp;"),
            '<' => escaped.push_str("&lt;"),
            '>' => escaped.push_str("&gt;"),
            '"' => escaped.push_str("&quot;"),
            '\'' => escaped.push_str("&#39;"),
            _ => escaped.push(character),
        }
    }
    escaped
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn records_richer_metrics_and_dashboard_snapshot() {
        let metrics = HttpMetrics::new(Some("athena_auth_observability_test"))
            .expect("metrics should initialize");

        metrics.start_request("GET", "/api/auth/session");
        metrics.finish_request(
            HttpRequestObservation::new("GET", "/api/auth/session", 200, Duration::from_millis(12))
                .with_response_body_bytes(Some(128)),
        );
        metrics.start_request("POST", "/api/auth/sign-in/email");
        metrics.finish_request(
            HttpRequestObservation::new(
                "POST",
                "/api/auth/sign-in/email",
                500,
                Duration::from_millis(37),
            )
            .with_request_body_bytes(Some(512))
            .with_response_body_bytes(Some(64)),
        );

        let (payload, content_type) = metrics.encode().expect("metrics should encode");
        assert!(
            content_type.contains("text/plain"),
            "Prometheus exposition should be plaintext"
        );
        assert!(
            payload.contains("http_requests_total"),
            "counter should be present"
        );
        assert!(
            payload.contains("http_requests_in_flight"),
            "in-flight gauge should be present"
        );
        assert!(
            payload.contains("http_request_errors_total"),
            "error counter should be present"
        );
        assert!(
            payload.contains("http_response_body_bytes"),
            "response body histogram should be present"
        );

        let snapshot = metrics.snapshot();
        assert_eq!(snapshot.total_requests, 2);
        assert_eq!(snapshot.in_flight_requests, 0);
        assert_eq!(snapshot.total_errors, 1);
        assert_eq!(snapshot.server_errors, 1);
        assert_eq!(snapshot.routes.len(), 2);

        let dashboard = metrics.render_dashboard("/metrics");
        assert!(dashboard.contains("athena-auth"));
        assert!(dashboard.contains("/api/auth/sign-in/email"));
        assert!(dashboard.contains("Prometheus scrape output"));
    }

    #[test]
    fn escapes_dashboard_route_values() {
        let metrics = HttpMetrics::new(None).expect("metrics should initialize");
        metrics.start_request("GET", "/search/<bad>");
        metrics.finish_request(HttpRequestObservation::new(
            "GET",
            "/search/<bad>",
            404,
            Duration::from_millis(1),
        ));

        let dashboard = metrics.render_dashboard("/metrics");
        assert!(dashboard.contains("/search/&lt;bad&gt;"));
        assert!(!dashboard.contains("<bad>"));
    }
}
