use std::{collections::BTreeMap, fmt::Write};

use super::{DurationMetricValue, escape_label, write_duration_metric};

/// Low-cardinality labels recorded for a Redis command.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RedisMetricLabels {
    /// Redis command category such as `GET`, `SETEX` or `DEL`.
    pub command: String,
    /// Redis shard name or `default`.
    pub shard: String,
    /// Result category, usually `success`, `error` or `timeout`.
    pub result: String,
}

impl RedisMetricLabels {
    /// Creates a Redis command label set without carrying Redis keys.
    pub fn new(
        command: impl Into<String>,
        shard: impl Into<String>,
        result: impl Into<String>,
    ) -> Self {
        Self {
            command: command.into(),
            shard: shard.into(),
            result: result.into(),
        }
    }
}

/// Low-cardinality labels recorded for Redis events such as pool, redirect or script cache.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RedisEventLabels {
    /// Event category.
    pub event: String,
    /// Redis shard name or `default`.
    pub shard: String,
    /// Result category.
    pub result: String,
}

impl RedisEventLabels {
    /// Creates a Redis event label set without carrying Redis keys.
    pub fn new(
        event: impl Into<String>,
        shard: impl Into<String>,
        result: impl Into<String>,
    ) -> Self {
        Self {
            event: event.into(),
            shard: shard.into(),
            result: result.into(),
        }
    }
}

/// Low-cardinality labels recorded when Redis behavior is explicitly degraded.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RedisDegradationLabels {
    /// Cache operation.
    pub operation: String,
    /// Degradation action.
    pub action: String,
    /// Redis shard name or `default`.
    pub shard: String,
}

impl RedisDegradationLabels {
    /// Creates a Redis degradation label set without carrying Redis keys.
    pub fn new(
        operation: impl Into<String>,
        action: impl Into<String>,
        shard: impl Into<String>,
    ) -> Self {
        Self {
            operation: operation.into(),
            action: action.into(),
            shard: shard.into(),
        }
    }
}

pub(crate) fn render(
    output: &mut String,
    metrics: &BTreeMap<RedisMetricLabels, DurationMetricValue>,
    events: &BTreeMap<RedisEventLabels, u64>,
    degradations: &BTreeMap<RedisDegradationLabels, u64>,
) {
    output.push_str("# HELP rs_zero_redis_commands_total Total number of Redis commands.\n");
    output.push_str("# TYPE rs_zero_redis_commands_total counter\n");
    for (labels, value) in metrics {
        write!(output, "rs_zero_redis_commands_total{{").ok();
        write_labels(output, labels, None);
        writeln!(output, "}} {}", value.count).ok();
    }

    output.push_str(
        "# HELP rs_zero_redis_command_errors_total Total number of failed Redis commands.\n",
    );
    output.push_str("# TYPE rs_zero_redis_command_errors_total counter\n");
    for (labels, value) in metrics
        .iter()
        .filter(|(labels, _)| labels.result != "success")
    {
        write!(output, "rs_zero_redis_command_errors_total{{").ok();
        write_labels(output, labels, None);
        writeln!(output, "}} {}", value.count).ok();
    }

    output.push_str("# HELP rs_zero_redis_command_duration_seconds Redis command duration.\n");
    output.push_str("# TYPE rs_zero_redis_command_duration_seconds histogram\n");
    for (labels, value) in metrics {
        write_duration_metric(
            output,
            "rs_zero_redis_command_duration_seconds",
            labels,
            value,
            write_labels,
        );
    }

    output.push_str("# HELP rs_zero_redis_events_total Total number of Redis pool, redirect, script and degradation events.\n");
    output.push_str("# TYPE rs_zero_redis_events_total counter\n");
    for (labels, value) in events {
        write!(output, "rs_zero_redis_events_total{{").ok();
        write_event_labels(output, labels);
        writeln!(output, "}} {}", value).ok();
    }

    output.push_str("# HELP rs_zero_redis_degradations_total Total number of explicitly degraded Redis operations.\n");
    output.push_str("# TYPE rs_zero_redis_degradations_total counter\n");
    for (labels, value) in degradations {
        write!(output, "rs_zero_redis_degradations_total{{").ok();
        write_degradation_labels(output, labels);
        writeln!(output, "}} {}", value).ok();
    }
}

fn write_labels(output: &mut String, labels: &RedisMetricLabels, le: Option<&str>) {
    write!(
        output,
        "command=\"{}\",shard=\"{}\",result=\"{}\"",
        escape_label(&labels.command),
        escape_label(&labels.shard),
        escape_label(&labels.result)
    )
    .ok();
    if let Some(le) = le {
        write!(output, ",le=\"{}\"", escape_label(le)).ok();
    }
}

fn write_event_labels(output: &mut String, labels: &RedisEventLabels) {
    write!(
        output,
        "event=\"{}\",shard=\"{}\",result=\"{}\"",
        escape_label(&labels.event),
        escape_label(&labels.shard),
        escape_label(&labels.result)
    )
    .ok();
}

fn write_degradation_labels(output: &mut String, labels: &RedisDegradationLabels) {
    write!(
        output,
        "operation=\"{}\",action=\"{}\",shard=\"{}\"",
        escape_label(&labels.operation),
        escape_label(&labels.action),
        escape_label(&labels.shard)
    )
    .ok();
}
