//! Prometheus metrics endpoint.
//!
//! Exposes server metrics in Prometheus text format.
//!
//! # Metrics Exposed
//!
//! - GraphQL queries (total, success, error)
//! - Query execution time (average)
//! - Database queries (total, duration)
//! - Error rates (validation, parse, execution)
//! - HTTP responses (2xx, 4xx, 5xx)
//! - Cache hit ratio

use std::fmt::Write as _;

use axum::{Json, extract::State, response::IntoResponse};
use fraiseql_core::db::traits::DatabaseAdapter;
use serde::Serialize;
use tracing::debug;

use crate::{metrics_server::PrometheusMetrics, routes::graphql::AppState};

/// Metrics response containing summary statistics.
#[derive(Debug, Serialize)]
pub struct MetricsResponse {
    /// Total GraphQL queries
    pub queries_total:           u64,
    /// Successfully executed queries
    pub queries_success:         u64,
    /// Failed queries
    pub queries_error:           u64,
    /// Average query duration (ms)
    pub avg_query_duration_ms:   f64,
    /// Cache hit ratio (0-1)
    pub cache_hit_ratio:         f64,
    /// Total connections in pool
    pub pool_connections_total:  u32,
    /// Idle (available) connections in pool
    pub pool_connections_idle:   u32,
    /// Active (in-use) connections in pool
    pub pool_connections_active: u32,
    /// Requests waiting for a pool connection
    pub pool_requests_waiting:   u32,
}

/// Metrics handler - returns Prometheus format metrics.
///
/// # Response
///
/// Returns metrics in Prometheus text-based format (text/plain).
/// Can be scraped by Prometheus or viewed in browser.
///
/// # Example Response
///
/// ```text
/// # HELP fraiseql_graphql_queries_total Total GraphQL queries executed
/// # TYPE fraiseql_graphql_queries_total counter
/// fraiseql_graphql_queries_total 1250
///
/// # HELP fraiseql_graphql_query_duration_ms Average query execution time in milliseconds
/// # TYPE fraiseql_graphql_query_duration_ms gauge
/// fraiseql_graphql_query_duration_ms 12.5
/// ```
pub async fn metrics_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
    State(state): State<AppState<A>>,
) -> impl IntoResponse {
    debug!("Metrics endpoint requested");

    // Collect metrics from AppState
    let prometheus_metrics = PrometheusMetrics::from(state.metrics.as_ref());
    let mut output = prometheus_metrics.to_prometheus_format();

    // Append per-entity federation circuit breaker state gauge (Step 8 of Issue #39)
    #[cfg(feature = "federation")]
    if let Some(ref cb_manager) = state.circuit_breaker {
        let states = cb_manager.collect_states();
        if !states.is_empty() {
            output.push_str(concat!(
                "\n# HELP fraiseql_federation_circuit_breaker_state ",
                "Federation circuit breaker state per entity type ",
                "(0=closed, 1=open, 2=half_open)\n",
                "# TYPE fraiseql_federation_circuit_breaker_state gauge\n",
            ));
            for (entity, state_code) in states {
                let _ = writeln!(
                    output,
                    "fraiseql_federation_circuit_breaker_state{{entity=\"{entity}\"}} \
                     {state_code}"
                );
            }
        }
    }

    // Append federation subgraph latency histogram and entity resolution counters.
    #[cfg(feature = "federation")]
    {
        let hist = state.federation_latency.to_prometheus_histogram();
        output.push('\n');
        output.push_str(&hist);
        let counters = state.federation_entity_metrics.to_prometheus_counters();
        output.push('\n');
        output.push_str(&counters);
    }

    // Append Redis rate-limiter error counter when the feature is compiled in.
    #[cfg(feature = "redis-rate-limiting")]
    {
        let errors = crate::middleware::rate_limit::redis_error_count_total();
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_rate_limit_redis_errors_total ",
                "Total Redis rate limiter fail-open events (Redis unreachable)\n",
                "# TYPE fraiseql_rate_limit_redis_errors_total counter\n",
                "fraiseql_rate_limit_redis_errors_total {errors}\n",
            ),
            errors = errors
        );
    }

    // Append Redis PKCE store error counter when the feature is compiled in.
    #[cfg(feature = "redis-pkce")]
    {
        let errors = fraiseql_auth::pkce::redis_pkce_error_count_total();
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_pkce_redis_errors_total ",
                "Total Redis PKCE store errors (connection failures, etc.)\n",
                "# TYPE fraiseql_pkce_redis_errors_total counter\n",
                "fraiseql_pkce_redis_errors_total {errors}\n",
            ),
            errors = errors
        );
    }

    // Append APQ (Automatic Persisted Queries) counters.
    {
        let apq = &state.apq_metrics;
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_apq_hits_total Total APQ cache hits\n",
                "# TYPE fraiseql_apq_hits_total counter\n",
                "fraiseql_apq_hits_total {hits}\n",
                "\n# HELP fraiseql_apq_misses_total Total APQ cache misses\n",
                "# TYPE fraiseql_apq_misses_total counter\n",
                "fraiseql_apq_misses_total {misses}\n",
                "\n# HELP fraiseql_apq_stored_total Total APQ queries stored\n",
                "# TYPE fraiseql_apq_stored_total counter\n",
                "fraiseql_apq_stored_total {stored}\n",
            ),
            hits = apq.get_hits(),
            misses = apq.get_misses(),
            stored = apq.get_stored(),
        );
    }

    // Append Redis APQ error counter when the feature is compiled in.
    #[cfg(feature = "redis-apq")]
    {
        let errors = fraiseql_core::apq::redis_storage::redis_apq_error_count_total();
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_apq_redis_errors_total ",
                "Total Redis APQ fail-open events\n",
                "# TYPE fraiseql_apq_redis_errors_total counter\n",
                "fraiseql_apq_redis_errors_total {errors}\n",
            ),
            errors = errors
        );
    }

    // Append MCP tool call counters when the feature is compiled in.
    #[cfg(feature = "mcp")]
    {
        let calls = crate::mcp::handler::mcp_tool_calls_total();
        let errors = crate::mcp::handler::mcp_tool_errors_total();
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_mcp_tool_calls_total Total MCP tool calls\n",
                "# TYPE fraiseql_mcp_tool_calls_total counter\n",
                "fraiseql_mcp_tool_calls_total {calls}\n",
                "\n# HELP fraiseql_mcp_tool_errors_total Total MCP tool call errors\n",
                "# TYPE fraiseql_mcp_tool_errors_total counter\n",
                "fraiseql_mcp_tool_errors_total {errors}\n",
            ),
            calls = calls,
            errors = errors,
        );
    }

    // Append trusted document counters.
    if state.trusted_docs.is_some() {
        let hits = crate::trusted_documents::hits_total();
        let misses = crate::trusted_documents::misses_total();
        let rejected = crate::trusted_documents::rejected_total();
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_trusted_documents_hits_total Trusted document lookups resolved from manifest\n",
                "# TYPE fraiseql_trusted_documents_hits_total counter\n",
                "fraiseql_trusted_documents_hits_total {hits}\n",
                "\n# HELP fraiseql_trusted_documents_misses_total Unknown document ID lookups\n",
                "# TYPE fraiseql_trusted_documents_misses_total counter\n",
                "fraiseql_trusted_documents_misses_total {misses}\n",
                "\n# HELP fraiseql_trusted_documents_rejected_total Raw queries rejected (strict mode)\n",
                "# TYPE fraiseql_trusted_documents_rejected_total counter\n",
                "fraiseql_trusted_documents_rejected_total {rejected}\n",
            ),
            hits = hits,
            misses = misses,
            rejected = rejected,
        );
    }

    // Pool health metrics (sampled live from adapter on each request)
    {
        let pool = state.executor().pool_metrics();
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_db_pool_connections_total Total connections in pool\n",
                "# TYPE fraiseql_db_pool_connections_total gauge\n",
                "fraiseql_db_pool_connections_total {total}\n",
                "\n# HELP fraiseql_db_pool_connections_idle Idle (available) connections\n",
                "# TYPE fraiseql_db_pool_connections_idle gauge\n",
                "fraiseql_db_pool_connections_idle {idle}\n",
                "\n# HELP fraiseql_db_pool_connections_active Active (in-use) connections\n",
                "# TYPE fraiseql_db_pool_connections_active gauge\n",
                "fraiseql_db_pool_connections_active {active}\n",
                "\n# HELP fraiseql_db_pool_requests_waiting Requests waiting for a connection\n",
                "# TYPE fraiseql_db_pool_requests_waiting gauge\n",
                "fraiseql_db_pool_requests_waiting {waiting}\n",
            ),
            total = pool.total_connections,
            idle = pool.idle_connections,
            active = pool.active_connections,
            waiting = pool.waiting_requests,
        );
    }

    // Database query performance stats (top 5 from pg_stat_statements / equivalent)
    if let Ok(stats) = state.executor().adapter().query_stats(5).await {
        if !stats.is_empty() {
            output.push_str(concat!(
                "\n# HELP fraiseql_db_query_exec_seconds ",
                "Cumulative execution time per query (from DB stats extension)\n",
                "# TYPE fraiseql_db_query_exec_seconds gauge\n",
            ));
            for entry in &stats {
                let query_short = truncate_label(&entry.query_text, 80);
                let _ = writeln!(
                    output,
                    "fraiseql_db_query_exec_seconds{{queryid=\"{}\",query=\"{}\"}} {:.6}",
                    entry.query_id,
                    query_short,
                    entry.total_exec_time_ms / 1000.0
                );
            }

            output.push_str(concat!(
                "\n# HELP fraiseql_db_query_calls ",
                "Total call count per query (from DB stats extension)\n",
                "# TYPE fraiseql_db_query_calls gauge\n",
            ));
            for entry in &stats {
                let query_short = truncate_label(&entry.query_text, 80);
                let _ = writeln!(
                    output,
                    "fraiseql_db_query_calls{{queryid=\"{}\",query=\"{}\"}} {}",
                    entry.query_id, query_short, entry.calls
                );
            }

            output.push_str(concat!(
                "\n# HELP fraiseql_db_query_mean_exec_seconds ",
                "Mean execution time per query (from DB stats extension)\n",
                "# TYPE fraiseql_db_query_mean_exec_seconds gauge\n",
            ));
            for entry in &stats {
                let query_short = truncate_label(&entry.query_text, 80);
                let _ = writeln!(
                    output,
                    "fraiseql_db_query_mean_exec_seconds{{queryid=\"{}\",query=\"{}\"}} {:.6}",
                    entry.query_id,
                    query_short,
                    entry.mean_exec_time_ms / 1000.0
                );
            }

            output.push_str(concat!(
                "\n# HELP fraiseql_db_cache_hit_ratio ",
                "Buffer cache hit ratio per query (0-1, from DB stats extension)\n",
                "# TYPE fraiseql_db_cache_hit_ratio gauge\n",
            ));
            for entry in &stats {
                if let Some(ratio) = entry.cache_hit_ratio {
                    let query_short = truncate_label(&entry.query_text, 80);
                    let _ = writeln!(
                        output,
                        "fraiseql_db_cache_hit_ratio{{queryid=\"{}\",query=\"{}\"}} {:.4}",
                        entry.query_id, query_short, ratio
                    );
                }
            }
        }
    }

    // Pool auto-tuner metrics (when enabled)
    if let Some(ref tuner) = state.pool_tuner {
        let adjustments = tuner.adjustments_total();
        let recommended = tuner.recommended_size();
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_pool_tuning_adjustments_total ",
                "Total pool resize operations applied or recommended\n",
                "# TYPE fraiseql_pool_tuning_adjustments_total counter\n",
                "fraiseql_pool_tuning_adjustments_total {adjustments}\n",
                "\n# HELP fraiseql_pool_recommended_size ",
                "Current recommended connection pool size\n",
                "# TYPE fraiseql_pool_recommended_size gauge\n",
                "fraiseql_pool_recommended_size {recommended}\n",
            ),
            adjustments = adjustments,
            recommended = recommended,
        );
    }

    // Schema reload counters
    {
        let reloads = state.metrics.schema_reloads_total.load(std::sync::atomic::Ordering::Relaxed);
        let errors = state
            .metrics
            .schema_reload_errors_total
            .load(std::sync::atomic::Ordering::Relaxed);
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_schema_reloads_total Total successful schema reloads\n",
                "# TYPE fraiseql_schema_reloads_total counter\n",
                "fraiseql_schema_reloads_total {reloads}\n",
                "\n# HELP fraiseql_schema_reload_errors_total Total failed schema reload attempts\n",
                "# TYPE fraiseql_schema_reload_errors_total counter\n",
                "fraiseql_schema_reload_errors_total {errors}\n",
            ),
            reloads = reloads,
            errors = errors,
        );
    }

    // Append per-operation histogram metrics
    output.push_str(&state.metrics.operation_metrics.to_prometheus_format());

    // HTTP request duration histogram
    output.push_str(&state.metrics.http_request_duration.to_prometheus_lines(
        "fraiseql_http_request_duration_seconds",
        "HTTP request duration in seconds",
    ));
    // Database query duration histogram
    output.push_str(&state.metrics.db_query_duration.to_prometheus_lines(
        "fraiseql_database_query_duration_seconds",
        "Database query duration in seconds",
    ));

    // JWT replay prevention counters.
    {
        let rejected = fraiseql_core::security::oidc::jwt_replay_rejected_total();
        let errors = fraiseql_core::security::oidc::jwt_replay_cache_errors_total();
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_jwt_replay_rejected_total ",
                "Total JWT replay attempts rejected (duplicate jti)\n",
                "# TYPE fraiseql_jwt_replay_rejected_total counter\n",
                "fraiseql_jwt_replay_rejected_total {rejected}\n",
                "\n# HELP fraiseql_jwt_replay_cache_errors_total ",
                "Total JWT replay cache backend errors (fail-open events)\n",
                "# TYPE fraiseql_jwt_replay_cache_errors_total counter\n",
                "fraiseql_jwt_replay_cache_errors_total {errors}\n",
            ),
            rejected = rejected,
            errors = errors,
        );
    }

    // Multi-root parallel query counter.
    {
        let multi_root = fraiseql_core::runtime::multi_root_queries_total();
        let _ = write!(
            output,
            concat!(
                "\n# HELP fraiseql_multi_root_queries_total ",
                "Total multi-root GraphQL queries dispatched via parallel execution\n",
                "# TYPE fraiseql_multi_root_queries_total counter\n",
                "fraiseql_multi_root_queries_total {multi_root}\n",
            ),
            multi_root = multi_root,
        );
    }

    // Append subscription counters.
    let subs = crate::routes::subscription_metrics();
    let _ = write!(
        output,
        concat!(
            "\n# HELP fraiseql_ws_connections_total Total WebSocket subscription connections\n",
            "# TYPE fraiseql_ws_connections_total counter\n",
            "fraiseql_ws_connections_total{{result=\"accepted\"}} {accepted}\n",
            "fraiseql_ws_connections_total{{result=\"rejected\"}} {rejected}\n",
            "\n# HELP fraiseql_ws_subscriptions_total Total subscription registrations\n",
            "# TYPE fraiseql_ws_subscriptions_total counter\n",
            "fraiseql_ws_subscriptions_total{{result=\"accepted\"}} {sub_accepted}\n",
            "fraiseql_ws_subscriptions_total{{result=\"rejected\"}} {sub_rejected}\n",
        ),
        accepted = subs.connections_accepted,
        rejected = subs.connections_rejected,
        sub_accepted = subs.subscriptions_accepted,
        sub_rejected = subs.subscriptions_rejected,
    );

    // Append `metrics`-facade emissions captured by the Prometheus recorder
    // (e.g. fraiseql-wire's ~40 counters/histograms). Empty unless the recorder
    // was installed at startup (audit H45).
    #[cfg(feature = "metrics")]
    {
        let facade = crate::metrics_recorder::render();
        if !facade.is_empty() {
            output.push('\n');
            output.push_str(&facade);
        }
    }

    (
        axum::http::StatusCode::OK,
        [("Content-Type", "text/plain; version=0.0.4")],
        output,
    )
}

/// Truncate a string for use as a Prometheus label value.
///
/// Escapes backslashes, double-quotes, and newlines per the Prometheus
/// exposition format, then truncates to `max_chars`.
fn truncate_label(s: &str, max_chars: usize) -> String {
    let truncated = if s.len() > max_chars {
        &s[..max_chars]
    } else {
        s
    };
    truncated.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n")
}

/// JSON metrics handler - returns metrics in JSON format.
///
/// Useful for dashboards and monitoring systems that consume JSON.
pub async fn metrics_json_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
    State(state): State<AppState<A>>,
) -> impl IntoResponse {
    debug!("JSON metrics endpoint requested");

    // Collect metrics from AppState
    let prometheus_metrics = PrometheusMetrics::from(state.metrics.as_ref());
    let pool = state.executor().pool_metrics();

    let response = MetricsResponse {
        queries_total:           prometheus_metrics.queries_total,
        queries_success:         prometheus_metrics.queries_success,
        queries_error:           prometheus_metrics.queries_error,
        avg_query_duration_ms:   prometheus_metrics.queries_avg_duration_ms,
        cache_hit_ratio:         prometheus_metrics.cache_hit_ratio,
        pool_connections_total:  pool.total_connections,
        pool_connections_idle:   pool.idle_connections,
        pool_connections_active: pool.active_connections,
        pool_requests_waiting:   pool.waiting_requests,
    };

    Json(response)
}
