use opentelemetry::{
    metrics::{Histogram, Meter},
    KeyValue,
};
use sonic_rs::Serialize;
use strum::IntoStaticStr;

#[cfg(debug_assertions)]
use crate::telemetry::metrics::catalog::debug_assert_attrs;
use crate::telemetry::metrics::catalog::{labels, names, units::DEMAND_CONTROL_COST_UNIT};

struct DemandControlInstruments {
    estimated_cost: Option<Histogram<u64>>,
    actual_cost: Option<Histogram<u64>>,
    delta: Option<Histogram<f64>>,
}

pub struct DemandControlMetrics {
    instruments: DemandControlInstruments,
}

#[derive(Debug, Clone)]
pub struct DemandControlMetricsRecorder {
    actual_cost: Histogram<u64>,
    delta: Histogram<f64>,
}

impl DemandControlMetrics {
    pub fn new(meter: Option<&Meter>) -> Self {
        let estimated_cost = meter.map(|meter| {
            meter
                .u64_histogram(names::COST_ESTIMATED)
                .with_unit(DEMAND_CONTROL_COST_UNIT)
                .with_description("The estimated cost of an operation before execution")
                .build()
        });

        let actual_cost = meter.map(|meter| {
            meter
                .u64_histogram(names::COST_ACTUAL)
                .with_unit(DEMAND_CONTROL_COST_UNIT)
                .with_description("The actual cost of an operation, measured after execution")
                .build()
        });

        let delta = meter.map(|meter| {
            meter
                .f64_histogram(names::COST_DELTA)
                .with_unit(DEMAND_CONTROL_COST_UNIT)
                .with_description("The difference between actual and estimated operation cost")
                .build()
        });

        Self {
            instruments: DemandControlInstruments {
                estimated_cost,
                actual_cost,
                delta,
            },
        }
    }

    pub fn record_estimated_cost(
        &self,
        cost: u64,
        result: &DemandControlResultCode,
        operation_name: Option<&str>,
    ) {
        let result_code: &'static str = result.into();
        let Some(histogram) = self.instruments.estimated_cost.as_ref() else {
            return;
        };

        let mut attrs = vec![KeyValue::new(labels::COST_RESULT, result_code.to_string())];
        if let Some(operation_name) = operation_name {
            attrs.push(KeyValue::new(
                labels::GRAPHQL_OPERATION_NAME,
                operation_name.to_string(),
            ));
        }
        #[cfg(debug_assertions)]
        debug_assert_attrs(names::COST_ESTIMATED, &attrs);
        histogram.record(cost, &attrs);
    }

    pub fn recorder(&self) -> Option<DemandControlMetricsRecorder> {
        let actual_cost = self.instruments.actual_cost.as_ref()?.clone();
        let delta = self.instruments.delta.as_ref()?.clone();

        Some(DemandControlMetricsRecorder { actual_cost, delta })
    }
}

#[derive(Debug, IntoStaticStr, Serialize, Clone, PartialEq, Eq)]
pub enum DemandControlResultCode {
    #[serde(rename = "COST_OK")]
    #[strum(serialize = "COST_OK")]
    CostOk,
    #[serde(rename = "COST_ESTIMATED_TOO_EXPENSIVE")]
    #[strum(serialize = "COST_ESTIMATED_TOO_EXPENSIVE")]
    CostEstimatedTooExpensive,
    #[serde(rename = "COST_ACTUAL_TOO_EXPENSIVE")]
    #[strum(serialize = "COST_ACTUAL_TOO_EXPENSIVE")]
    CostActualTooExpensive,
    #[serde(rename = "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE")]
    #[strum(serialize = "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE")]
    SubgraphCostEstimatedTooExpensive,
}

impl DemandControlResultCode {
    pub fn as_str(&self) -> &'static str {
        self.into()
    }

    pub fn from_artifacts(max: u64, estimated: u64, actual: u64) -> Self {
        if estimated > max {
            return Self::CostEstimatedTooExpensive;
        }

        if actual > max {
            return Self::CostActualTooExpensive;
        }

        Self::CostOk
    }
}

impl DemandControlMetricsRecorder {
    pub fn record_actual_cost(
        &self,
        cost: u64,
        result: &DemandControlResultCode,
        operation_name: Option<&str>,
    ) {
        let result_code: &'static str = result.into();
        let mut attrs = vec![KeyValue::new(labels::COST_RESULT, result_code.to_string())];
        if let Some(operation_name) = operation_name {
            attrs.push(KeyValue::new(
                labels::GRAPHQL_OPERATION_NAME,
                operation_name.to_string(),
            ));
        }
        #[cfg(debug_assertions)]
        debug_assert_attrs(names::COST_ACTUAL, &attrs);
        self.actual_cost.record(cost, &attrs);
    }

    pub fn record_delta(
        &self,
        delta: f64,
        result: &DemandControlResultCode,
        operation_name: Option<&str>,
    ) {
        let result_code: &'static str = result.into();
        let mut attrs = vec![KeyValue::new(labels::COST_RESULT, result_code.to_string())];
        if let Some(operation_name) = operation_name {
            attrs.push(KeyValue::new(
                labels::GRAPHQL_OPERATION_NAME,
                operation_name.to_string(),
            ));
        }
        #[cfg(debug_assertions)]
        debug_assert_attrs(names::COST_DELTA, &attrs);
        self.delta.record(delta, &attrs);
    }
}
