//! Lightweight Prometheus HTTP metrics server for standalone binaries.
//!
//! Services that already embed Axum/tonic can expose `/metrics` through their
//! existing HTTP stack.  Training binaries and CLI tools that don't have an
//! HTTP server use this instead.

use std::io::{BufRead, BufReader, Read as IoRead, Write as IoWrite};
use std::net::{TcpListener, TcpStream};

use super::gather_metrics;

/// Spawn a background daemon thread serving Prometheus metrics over HTTP.
///
/// Responds to `GET /metrics` with the global registry output in Prometheus
/// text exposition format.  Any other request gets a 404.  The thread is
/// detached and dies with the process.
///
/// Safety hardening:
/// - 5-second read timeout prevents slow-loris connections
/// - 8KB request line limit prevents memory abuse
/// - Correct Content-Type charset for Prometheus scrapers
pub fn start_metrics_server(port: u16) {
    std::thread::spawn(move || {
        let addr = format!("0.0.0.0:{port}");
        let listener = match TcpListener::bind(&addr) {
            Ok(l) => {
                tracing::info!("Prometheus metrics server listening on {addr}");
                l
            }
            Err(e) => {
                tracing::warn!("Failed to bind metrics server on {addr}: {e}");
                return;
            }
        };

        for stream in listener.incoming() {
            let Ok(mut stream) = stream else {
                continue;
            };

            // 5-second read timeout prevents slow-loris connections
            _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(5)));

            // Read the HTTP request line, limited to 8KB to prevent memory abuse
            let mut request_line = String::new();
            if BufReader::new((&stream).take(8192))
                .read_line(&mut request_line)
                .is_err()
            {
                continue;
            }

            let is_metrics = request_line.starts_with("GET /metrics");

            if is_metrics {
                let body = gather_metrics();
                let response = format!(
                    "HTTP/1.1 200 OK\r\n\
                     Content-Type: text/plain; version=0.0.4; charset=utf-8\r\n\
                     Content-Length: {}\r\n\r\n\
                     {}",
                    body.len(),
                    body,
                );
                _ = stream.write_all(response.as_bytes());
            } else {
                _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n");
            }
        }
    });
}

/// Push all registered metrics to a Prometheus Pushgateway.
///
/// Uses a plain TCP connection to POST the text exposition format.
/// The `gateway` address should be `host:port` (e.g. `pushgateway.foxhunt.svc.cluster.local:9091`).
/// `job` is the grouping key (typically the binary name like `hyperopt_baseline_rl`).
///
/// Falls back to the `PUSHGATEWAY_URL` env var if `gateway` is `None`.
/// Returns `Ok(())` on success, logs and returns error on failure.
pub fn push_to_gateway(gateway: Option<&str>, job: &str) -> Result<(), String> {
    let addr = match gateway {
        Some(a) => a.to_string(),
        None => std::env::var("PUSHGATEWAY_URL")
            .unwrap_or_else(|_| "pushgateway.foxhunt.svc.cluster.local:9091".to_string()),
    };

    let body = gather_metrics();
    if body.is_empty() {
        tracing::debug!("No metrics to push to gateway");
        return Ok(());
    }

    let path = format!("/metrics/job/{job}");
    let request = format!(
        "POST {path} HTTP/1.1\r\n\
         Host: {addr}\r\n\
         Content-Type: text/plain; version=0.0.4\r\n\
         Content-Length: {}\r\n\
         Connection: close\r\n\r\n\
         {body}",
        body.len(),
    );

    let mut stream = TcpStream::connect(&addr).map_err(|e| {
        let msg = format!("Failed to connect to pushgateway at {addr}: {e}");
        tracing::warn!("{}", msg);
        msg
    })?;

    stream.write_all(request.as_bytes()).map_err(|e| {
        let msg = format!("Failed to send metrics to pushgateway: {e}");
        tracing::warn!("{}", msg);
        msg
    })?;

    // Read response status line
    let mut response = String::new();
    let _ = BufReader::new(&stream).read_line(&mut response);

    if response.contains("200") || response.contains("202") {
        tracing::info!("Pushed metrics to pushgateway ({addr}) for job={job}");
        Ok(())
    } else {
        let msg = format!("Pushgateway returned unexpected response: {}", response.trim());
        tracing::warn!("{}", msg);
        Err(msg)
    }
}
