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

use prometheus_client::metrics::family::{Family, MetricConstructor};
use prometheus_client::metrics::histogram::Histogram;
use pyo3::prelude::*;

use crate::labels::{args_to_label_set, kwargs_to_label_set, validate_label_names};
use crate::registry::{MetricOpts, PyRegistry, register_metric};

const DEFAULT_BUCKETS: &[f64] = &[
    0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0,
];

#[derive(Clone)]
struct HistogramConstructor {
    buckets: Vec<f64>,
}

impl MetricConstructor<Histogram> for HistogramConstructor {
    fn new_metric(&self) -> Histogram {
        Histogram::new(self.buckets.iter().copied())
    }
}

type HistogramFamily = Family<Vec<(String, String)>, Histogram, HistogramConstructor>;

#[pyclass(name = "Histogram", subclass)]
pub struct PyHistogram {
    family: Arc<HistogramFamily>,
    label_names: Vec<String>,
    no_label_metric: Option<Histogram>,
}

#[pymethods]
impl PyHistogram {
    #[new]
    #[pyo3(signature = (name, documentation, labelnames=vec![], *, namespace="", subsystem="", unit="", registry=None, buckets=None))]
    fn new(
        name: &str,
        documentation: &str,
        labelnames: Vec<String>,
        namespace: &str,
        subsystem: &str,
        unit: &str,
        registry: Option<&PyRegistry>,
        buckets: Option<Vec<f64>>,
    ) -> PyResult<Self> {
        let buckets = buckets.unwrap_or_else(|| DEFAULT_BUCKETS.to_vec());
        let constructor = HistogramConstructor { buckets };
        let family = Arc::new(HistogramFamily::new_with_constructor(constructor));
        let opts = MetricOpts {
            name,
            help: documentation,
            namespace,
            subsystem,
            unit,
            registry,
        };
        register_metric(&opts, (*family).clone())?;
        let no_label_metric = if labelnames.is_empty() {
            Some(family.get_or_create_owned(&vec![]))
        } else {
            None
        };
        Ok(Self {
            family,
            label_names: labelnames,
            no_label_metric,
        })
    }

    #[classattr]
    #[pyo3(name = "DEFAULT_BUCKETS")]
    fn default_buckets() -> Vec<f64> {
        let mut buckets = DEFAULT_BUCKETS.to_vec();
        buckets.push(f64::INFINITY);
        buckets
    }

    #[pyo3(signature = (*args, **kwargs))]
    fn labels(
        &self,
        args: Vec<String>,
        kwargs: Option<HashMap<String, String>>,
    ) -> PyResult<PyHistogramChild> {
        let label_set = if args.is_empty() {
            let kwargs = kwargs.unwrap_or_default();
            validate_label_names(&self.label_names, &kwargs)
                .map_err(pyo3::exceptions::PyValueError::new_err)?;
            kwargs_to_label_set(kwargs)
        } else {
            if kwargs.is_some_and(|k| !k.is_empty()) {
                return Err(pyo3::exceptions::PyValueError::new_err(
                    "Cannot use both positional and keyword arguments for labels",
                ));
            }
            args_to_label_set(&self.label_names, args)
                .map_err(pyo3::exceptions::PyValueError::new_err)?
        };
        let metric = self.family.get_or_create_owned(&label_set);
        Ok(PyHistogramChild { metric })
    }

    fn observe(&self, value: f64) -> PyResult<()> {
        let metric = self.no_label_metric.as_ref().ok_or_else(|| {
            pyo3::exceptions::PyValueError::new_err(
                "No label values were provided. Use .labels() to provide label values.",
            )
        })?;
        metric.observe(value);
        Ok(())
    }
}

#[pyclass(name = "_HistogramChild")]
pub struct PyHistogramChild {
    metric: Histogram,
}

#[pymethods]
impl PyHistogramChild {
    fn observe(&self, value: f64) {
        self.metric.observe(value);
    }
}
