//! In-process Prometheus-style metrics for the portal.
//!
//! CONTRACT: metric names and label keys are part of the operational contract
//! used by dashboards and smoke checks. Additive metrics are allowed; renaming
//! existing metrics requires synchronized dashboard/documentation changes.

use std::collections::BTreeMap;
use std::fmt::Write as FmtWrite;
use std::sync::{Mutex, OnceLock};

use tiny_http::StatusCode;

use super::readiness::build_readyz;
use super::request_context::HttpRequestMetadata;
use crate::Cli;

static PORTAL_METRICS: OnceLock<Mutex<PortalMetrics>> = OnceLock::new();

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

#[derive(Clone, Debug, Default)]
struct HttpMetricValue {
    requests_total: u64,
    duration_seconds_sum: f64,
    duration_seconds_count: u64,
}

#[derive(Clone, Debug, Default)]
struct PortalMetrics {
    http: BTreeMap<HttpMetricKey, HttpMetricValue>,
    reports_generated_total: u64,
    ingestion_records_total: u64,
    ingestion_rejected_total: u64,
    role_denied_total: u64,
}

fn portal_metrics() -> &'static Mutex<PortalMetrics> {
    PORTAL_METRICS.get_or_init(|| Mutex::new(PortalMetrics::default()))
}

pub(crate) fn record_http_metric(metadata: &HttpRequestMetadata, status: StatusCode) {
    if let Ok(mut metrics) = portal_metrics().lock() {
        let entry = metrics
            .http
            .entry(HttpMetricKey {
                method: metadata.method.clone(),
                route: metadata.route.clone(),
                status: status.0,
                module: metadata.module.clone(),
            })
            .or_default();
        entry.requests_total = entry.requests_total.saturating_add(1);
        entry.duration_seconds_sum += metadata.latency_ms as f64 / 1_000.0;
        entry.duration_seconds_count = entry.duration_seconds_count.saturating_add(1);
        if status.0 == 403 {
            metrics.role_denied_total = metrics.role_denied_total.saturating_add(1);
        }
    }
}

pub(crate) fn record_report_generated() {
    if let Ok(mut metrics) = portal_metrics().lock() {
        metrics.reports_generated_total = metrics.reports_generated_total.saturating_add(1);
    }
}

pub(crate) fn record_ingestion_accepted() {
    if let Ok(mut metrics) = portal_metrics().lock() {
        metrics.ingestion_records_total = metrics.ingestion_records_total.saturating_add(1);
    }
}

pub(crate) fn record_ingestion_rejected() {
    if let Ok(mut metrics) = portal_metrics().lock() {
        metrics.ingestion_rejected_total = metrics.ingestion_rejected_total.saturating_add(1);
    }
}

pub(crate) fn render_prometheus_metrics(args: &Cli) -> String {
    let mut text = String::new();
    let ready_value = if build_readyz(args)
        .get("status")
        .and_then(serde_json::Value::as_str)
        .unwrap_or("not_ready")
        == "ready"
    {
        1
    } else {
        0
    };
    writeln!(
        &mut text,
        "# HELP awatch_http_requests_total HTTP requests handled by AWatch-rus portal"
    )
    .ok();
    writeln!(&mut text, "# TYPE awatch_http_requests_total counter").ok();
    let metrics = portal_metrics()
        .lock()
        .map(|guard| guard.clone())
        .unwrap_or_default();
    for (key, value) in &metrics.http {
        writeln!(
            &mut text,
            "awatch_http_requests_total{{method=\"{}\",route=\"{}\",status=\"{}\",module=\"{}\"}} {}",
            prom_escape(&key.method),
            prom_escape(&key.route),
            key.status,
            prom_escape(&key.module),
            value.requests_total
        )
        .ok();
    }
    writeln!(
        &mut text,
        "# HELP awatch_http_request_duration_seconds HTTP request duration in seconds"
    )
    .ok();
    writeln!(
        &mut text,
        "# TYPE awatch_http_request_duration_seconds summary"
    )
    .ok();
    for (key, value) in &metrics.http {
        writeln!(
            &mut text,
            "awatch_http_request_duration_seconds_sum{{method=\"{}\",route=\"{}\",status=\"{}\",module=\"{}\"}} {:.6}",
            prom_escape(&key.method),
            prom_escape(&key.route),
            key.status,
            prom_escape(&key.module),
            value.duration_seconds_sum
        )
        .ok();
        writeln!(
            &mut text,
            "awatch_http_request_duration_seconds_count{{method=\"{}\",route=\"{}\",status=\"{}\",module=\"{}\"}} {}",
            prom_escape(&key.method),
            prom_escape(&key.route),
            key.status,
            prom_escape(&key.module),
            value.duration_seconds_count
        )
        .ok();
    }
    for (name, help, value) in [
        (
            "awatch_reports_generated_total",
            "Reports generated by the portal",
            metrics.reports_generated_total,
        ),
        (
            "awatch_ingestion_records_total",
            "Telemetry ingestion records accepted",
            metrics.ingestion_records_total,
        ),
        (
            "awatch_ingestion_rejected_total",
            "Telemetry ingestion records rejected",
            metrics.ingestion_rejected_total,
        ),
        (
            "awatch_role_denied_total",
            "Requests denied by role gates",
            metrics.role_denied_total,
        ),
    ] {
        writeln!(&mut text, "# HELP {name} {help}").ok();
        writeln!(&mut text, "# TYPE {name} counter").ok();
        writeln!(&mut text, "{name} {value}").ok();
    }
    writeln!(
        &mut text,
        "# HELP awatch_readyz_status Portal readiness status, 1=ready, 0=not_ready"
    )
    .ok();
    writeln!(&mut text, "# TYPE awatch_readyz_status gauge").ok();
    writeln!(&mut text, "awatch_readyz_status {ready_value}").ok();
    text
}

fn prom_escape(value: &str) -> String {
    // SECURITY: metric label values are route/module tokens, but escaping keeps
    // the endpoint safe if future callers pass proxy-derived values.
    value.replace('\\', "\\\\").replace('"', "\\\"")
}
