use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::time::{SystemTime, UNIX_EPOCH};

use prometheus_client::metrics::family::Family;
use prometheus_client::metrics::gauge::Gauge;
use pyo3::prelude::*;

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

type GaugeFamily = Family<Vec<(String, String)>, Gauge<f64, AtomicU64>>;

fn current_time_secs() -> PyResult<f64> {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs_f64())
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
}

#[pyclass(name = "Gauge", subclass)]
pub struct PyGauge {
    family: Arc<GaugeFamily>,
    label_names: Vec<String>,
    no_label_metric: Option<Gauge<f64, AtomicU64>>,
}

#[pymethods]
impl PyGauge {
    #[new]
    #[pyo3(signature = (name, documentation, labelnames=vec![], *, namespace="", subsystem="", unit="", registry=None))]
    fn new(
        name: &str,
        documentation: &str,
        labelnames: Vec<String>,
        namespace: &str,
        subsystem: &str,
        unit: &str,
        registry: Option<&PyRegistry>,
    ) -> PyResult<Self> {
        let family = Arc::new(GaugeFamily::default());
        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,
        })
    }

    #[pyo3(signature = (*args, **kwargs))]
    fn labels(
        &self,
        args: Vec<String>,
        kwargs: Option<HashMap<String, String>>,
    ) -> PyResult<PyGaugeChild> {
        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(PyGaugeChild { metric })
    }

    fn set(&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.set(value);
        Ok(())
    }

    #[pyo3(signature = (amount=1.0))]
    fn inc(&self, amount: 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.inc_by(amount);
        Ok(())
    }

    #[pyo3(signature = (amount=1.0))]
    fn dec(&self, amount: 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.dec_by(amount);
        Ok(())
    }

    fn set_to_current_time(&self) -> 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.set(current_time_secs()?);
        Ok(())
    }
}

#[pyclass(name = "_GaugeChild")]
pub struct PyGaugeChild {
    metric: Gauge<f64, AtomicU64>,
}

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

    #[pyo3(signature = (amount=1.0))]
    fn inc(&self, amount: f64) {
        self.metric.inc_by(amount);
    }

    #[pyo3(signature = (amount=1.0))]
    fn dec(&self, amount: f64) {
        self.metric.dec_by(amount);
    }

    fn set_to_current_time(&self) -> PyResult<()> {
        self.metric.set(current_time_secs()?);
        Ok(())
    }
}
