//! Redis-backed metrics exporter.
//!
//! Every `interval` seconds this module pushes metric fields to a Redis hash
//! bucketed by day:
//!   Key:  `metrics:{instance_id}:{YYYY-MM-DD}`  (hash)
//!   TTL:  30 days per day bucket
//!
//! Also maintains a sorted set `metrics:index:{instance_id}:{YYYY-MM-DD}` per
//! day for querying within a day window.

use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;

use deadpool_redis::cluster::Pool;
use serde::{Deserialize, Serialize};

const METRICS_TTL_SECS: i64 = 30 * 24 * 60 * 60; // 30 days

/// A snapshot of metric values at a point in time.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsSnapshot {
    pub instance_id: String,
    pub timestamp_secs: i64,
    #[serde(flatten)]
    pub http: HashMap<String, serde_json::Value>,
    #[serde(flatten)]
    pub room: HashMap<String, serde_json::Value>,
}

/// Starts a background task that snapshots and pushes all metrics to Redis
/// every `interval` seconds.
///
/// `prometheus_handle`: from `install_recorder()`, used to render RoomMetrics values
/// `http_metrics`: shared `HttpMetrics` from `MetricsMiddleware`
/// `instance_id`: used as the Redis key prefix
pub fn spawn_redis_metrics_flusher(
    redis_pool: Pool,
    instance_id: String,
    prometheus_handle: Arc<metrics_exporter_prometheus::PrometheusHandle>,
    http_metrics: Arc<crate::metrics_middleware::HttpMetrics>,
    interval: Duration,
) {
    tokio::spawn(async move {
        let mut ticker = tokio::time::interval(interval);
        loop {
            ticker.tick().await;
            if let Err(e) = flush_metrics(&redis_pool, &instance_id, &prometheus_handle, http_metrics.as_ref()).await {
                tracing::warn!(error = %e, "failed to flush metrics to Redis");
            }
        }
    });
}

async fn flush_metrics(
    pool: &Pool,
    instance_id: &str,
    prometheus_handle: &Arc<metrics_exporter_prometheus::PrometheusHandle>,
    http_metrics: &crate::metrics_middleware::HttpMetrics,
) -> anyhow::Result<()> {
    let now = chrono::Utc::now();
    let now_ts = now.timestamp();
    let day_key = now.format("%Y-%m-%d").to_string();

    // RoomMetrics from the metrics crate (rendered via PrometheusHandle)
    let body = prometheus_handle.render();
    let room = crate::prometheus_exporter::render_to_hashmap(&body);

    // HTTP metrics from AtomicU64
    let http = http_metrics.snapshot();

    let hash_key = format!("metrics:{}:{}", instance_id, day_key);
    let index_key = format!("metrics:index:{}:{}", instance_id, day_key);

    let mut conn = pool.get().await?;

    // HSET each metric field into the daily hash
    let mut fields: Vec<(String, String)> = Vec::with_capacity(http.len() + room.len());
    for (k, v) in &http {
        fields.push((format!("http_{}", k), v.to_string()));
    }
    for (k, v) in &room {
        fields.push((format!("room_{}", k), v.to_string()));
    }

    // HSET field field_value ...
    let mut cmd = redis::cmd("HSET");
    cmd.arg(&hash_key);
    for (f, val) in &fields {
        cmd.arg(f).arg(val);
    }
    let _: () = cmd.query_async(&mut conn).await?;

    // Set TTL on the hash key (refresh every write)
    let _: () = redis::cmd("EXPIRE")
        .arg(&hash_key)
        .arg(METRICS_TTL_SECS)
        .query_async(&mut conn)
        .await?;

    // Update sorted set index for this instance+day
    let _: () = redis::cmd("ZADD")
        .arg(&index_key)
        .arg(now_ts as f64)
        .arg(now_ts.to_string())
        .query_async(&mut conn)
        .await?;

    // Trim index to last 1000 entries per day
    let _: () = redis::cmd("ZREMRANGEBYRANK")
        .arg(&index_key)
        .arg(0)
        .arg(-1001)
        .query_async(&mut conn)
        .await?;

    // Set TTL on the index key
    let _: () = redis::cmd("EXPIRE")
        .arg(&index_key)
        .arg(METRICS_TTL_SECS)
        .query_async(&mut conn)
        .await?;

    tracing::debug!(key = %hash_key, field_count = fields.len(), "metrics flushed to Redis (daily hash)");
    Ok(())
}

/// Query metrics for an instance from Redis, reading the most recent `limit`
/// snapshots across all available daily hash buckets.
/// Returns a vector sorted by timestamp ascending.
pub async fn query_instance_metrics(
    pool: &Pool,
    instance_id: &str,
    limit: usize,
) -> anyhow::Result<Vec<(i64, MetricsSnapshot)>> {
    let mut conn = pool.get().await?;

    // Scan for all daily index keys for this instance
    let index_pattern = format!("metrics:index:{}:*", instance_id);
    let mut cursor = 0u64;
    let mut day_keys: Vec<String> = Vec::new();
    loop {
        let (new_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
            .arg(cursor)
            .arg("MATCH")
            .arg(&index_pattern)
            .arg("COUNT")
            .arg(100)
            .query_async(&mut conn)
            .await?;
        day_keys.extend(keys);
        cursor = new_cursor;
        if cursor == 0 {
            break;
        }
    }

    // Collect all timestamps from all daily indexes
    let mut all_timestamps: Vec<i64> = Vec::new();
    for day_key in &day_keys {
        let timestamps: Vec<i64> = redis::cmd("ZREVRANGE")
            .arg(day_key)
            .arg(0)
            .arg((limit as isize - 1).max(0))
            .query_async(&mut conn)
            .await?;
        all_timestamps.extend(timestamps);
    }

    // Sort descending and take limit
    all_timestamps.sort_by_key(|&ts| std::cmp::Reverse(ts));
    all_timestamps.truncate(limit);

    // Read each snapshot from the daily hash
    let mut results = Vec::with_capacity(all_timestamps.len());
    for ts in &all_timestamps {
        // Derive day bucket from timestamp
        let dt = chrono::DateTime::from_timestamp(*ts, 0)
            .unwrap_or_else(|| chrono::Utc::now());
        let day_str = dt.format("%Y-%m-%d").to_string();
        let hash_key = format!("metrics:{}:{}", instance_id, day_str);

        let fields: Option<HashMap<String, String>> = redis::cmd("HGETALL")
            .arg(&hash_key)
            .query_async(&mut conn)
            .await?;

        if let Some(fields) = fields {
            let mut http: HashMap<String, serde_json::Value> = HashMap::new();
            let mut room: HashMap<String, serde_json::Value> = HashMap::new();
            for (k, v) in &fields {
                let json_val: serde_json::Value = serde_json::from_str(v).unwrap_or_else(|_| serde_json::Value::String(v.clone()));
                        if let Some(stripped) = k.strip_prefix("http_") {
                            http.insert(stripped.to_string(), json_val);
                        } else if let Some(stripped) = k.strip_prefix("room_") {
                            room.insert(stripped.to_string(), json_val);
                        }
            }
            results.push((*ts, MetricsSnapshot {
                instance_id: instance_id.to_string(),
                timestamp_secs: *ts,
                http,
                room,
            }));
        }
    }

    results.sort_by_key(|(ts, _)| *ts);
    Ok(results)
}

/// Flat instance metrics suitable for JSON serialization.
#[derive(serde::Serialize)]
pub struct FlatInstanceMetrics {
    pub instance_id: String,
    pub timestamp_secs: i64,
    pub http: std::collections::HashMap<String, String>,
    pub room: std::collections::HashMap<String, String>,
}

/// Query all instances' metrics, optionally filtered by `instance_filter`.
pub async fn query_all_instance_metrics(
    pool: &Pool,
    instance_filter: &str,
    limit: usize,
) -> anyhow::Result<Vec<FlatInstanceMetrics>> {
    let mut conn = pool.get().await?;

    // Scan for all daily index keys to extract instance ids
    let mut cursor = 0u64;
    let mut all_instance_ids = Vec::new();
    let mut seen = std::collections::HashSet::new();
    loop {
        let (new_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
            .arg(cursor)
            .arg("MATCH")
            .arg("metrics:index:*")
            .arg("COUNT")
            .arg(100)
            .query_async(&mut conn)
            .await?;
        for k in keys {
            // Key format: metrics:index:{instance}:{YYYY-MM-DD}
            if let Some(rest) = k.strip_prefix("metrics:index:") {
                if let Some((instance_id, _)) = rest.rsplit_once(':') {
                    if seen.insert(instance_id.to_string()) {
                        all_instance_ids.push(instance_id.to_string());
                    }
                }
            }
        }
        cursor = new_cursor;
        if cursor == 0 {
            break;
        }
    }

    let mut results = Vec::new();
    for instance_id in all_instance_ids {
        if !instance_filter.is_empty() && !instance_id.contains(instance_filter) {
            continue;
        }
        let snapshots = query_instance_metrics(pool, &instance_id, limit).await?;
        for (_, payload) in snapshots {
            let mut http: std::collections::HashMap<String, String> = std::collections::HashMap::new();
            let mut room: std::collections::HashMap<String, String> = std::collections::HashMap::new();
            for (k, v) in &payload.http {
                http.insert(k.clone(), v.to_string());
            }
            for (k, v) in &payload.room {
                room.insert(k.clone(), v.to_string());
            }
            results.push(FlatInstanceMetrics {
                instance_id: payload.instance_id,
                timestamp_secs: payload.timestamp_secs,
                http,
                room,
            });
        }
    }

    Ok(results)
}

/// Export all metrics across all known instances as CSV.
pub async fn export_all_metrics_csv(
    pool: &Pool,
    instance_filter: &str,
) -> anyhow::Result<String> {
    let mut conn = pool.get().await?;

    // Scan for all daily index keys to extract instance ids
    let mut cursor = 0u64;
    let mut all_instance_ids = Vec::new();
    let mut seen = std::collections::HashSet::new();
    loop {
        let (new_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
            .arg(cursor)
            .arg("MATCH")
            .arg("metrics:index:*")
            .arg("COUNT")
            .arg(100)
            .query_async(&mut conn)
            .await?;
        for k in keys {
            // Key format: metrics:index:{instance}:{YYYY-MM-DD}
            if let Some(rest) = k.strip_prefix("metrics:index:") {
                if let Some(instance_id) = rest.rsplit_once(':') {
                    let id = instance_id.0;
                    if seen.insert(id.to_string()) {
                        all_instance_ids.push(id.to_string());
                    }
                }
            }
        }
        cursor = new_cursor;
        if cursor == 0 {
            break;
        }
    }

    let mut rows: Vec<String> = vec![
        "instance_id,timestamp,metric,value".to_string(),
    ];

    for instance_id in all_instance_ids {
        if !instance_filter.is_empty() && !instance_id.contains(instance_filter) {
            continue;
        }
        let metrics = query_instance_metrics(pool, &instance_id, 1000).await?;
        for (ts, payload) in metrics {
            for (k, v) in &payload.http {
                rows.push(format!("{},{},http_{},{}", instance_id, ts, k, v));
            }
            for (k, v) in &payload.room {
                rows.push(format!("{},{},room_{},{}", instance_id, ts, k, v));
            }
        }
    }

    Ok(rows.join("\n"))
}
