// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use inherent::inherent;
use std::sync::Arc;

use glean_core::metrics::MetricType;
use glean_core::ErrorType;

/// Sealed traits protect against downstream implementations.
///
/// We wrap it in a private module that is inaccessible outside of this module.
mod private {
    use crate::{
        private::BooleanMetric, private::CounterMetric, private::StringMetric, CommonMetricData,
    };
    use std::sync::Arc;

    /// The sealed labeled trait.
    ///
    /// This also allows us to hide methods, that are only used internally
    /// and should not be visible to users of the object implementing the
    /// `Labeled<T>` trait.
    pub trait Sealed {
        /// The `glean_core` metric type representing the labeled metric.
        type Inner: glean_core::metrics::MetricType + Clone;

        /// Create a new metric object implementing this trait from the inner type.
        fn from_inner(metric: Self::Inner) -> Self;

        /// Create a new `glean_core` metric from the metadata.
        fn new_inner(meta: crate::CommonMetricData) -> Self::Inner;
    }

    // `LabeledMetric<BooleanMetric>` is possible.
    //
    // See [Labeled Booleans](https://mozilla.github.io/glean/book/user/metrics/labeled_booleans.html).
    impl Sealed for BooleanMetric {
        type Inner = glean_core::metrics::BooleanMetric;

        fn from_inner(metric: Self::Inner) -> Self {
            BooleanMetric(Arc::new(metric))
        }

        fn new_inner(meta: CommonMetricData) -> Self::Inner {
            glean_core::metrics::BooleanMetric::new(meta)
        }
    }

    // `LabeledMetric<StringMetric>` is possible.
    //
    // See [Labeled Strings](https://mozilla.github.io/glean/book/user/metrics/labeled_strings.html).
    impl Sealed for StringMetric {
        type Inner = glean_core::metrics::StringMetric;

        fn from_inner(metric: Self::Inner) -> Self {
            StringMetric(Arc::new(metric))
        }

        fn new_inner(meta: CommonMetricData) -> Self::Inner {
            glean_core::metrics::StringMetric::new(meta)
        }
    }

    // `LabeledMetric<CounterMetric>` is possible.
    //
    // See [Labeled Counters](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html).
    impl Sealed for CounterMetric {
        type Inner = glean_core::metrics::CounterMetric;

        fn from_inner(metric: Self::Inner) -> Self {
            CounterMetric(Arc::new(metric))
        }

        fn new_inner(meta: CommonMetricData) -> Self::Inner {
            glean_core::metrics::CounterMetric::new(meta)
        }
    }
}

/// Marker trait for metrics that can be nested inside a labeled metric.
///
/// This trait is sealed and cannot be implemented for types outside this crate.
pub trait AllowLabeled: private::Sealed {}

// Implement the trait for everything we marked as allowed.
impl<T> AllowLabeled for T where T: private::Sealed {}

// We need to wrap the glean-core type: otherwise if we try to implement
// the trait for the metric in `glean_core::metrics` we hit error[E0117]:
// only traits defined in the current crate can be implemented for arbitrary
// types.

/// Developer-facing API for recording labeled metrics.
///
/// Instances of this type are automatically generated by the parser
/// at build time, allowing developers to record values that were previously
/// registered in the metrics.yaml file.
/// Unlike most metric types, [`LabeledMetric`] does not have its own corresponding
/// storage, but records metrics for the underlying metric type `T` in the storage
/// for that type.
#[derive(Clone)]
pub struct LabeledMetric<T: AllowLabeled>(
    pub(crate) Arc<glean_core::metrics::LabeledMetric<T::Inner>>,
);

impl<T> LabeledMetric<T>
where
    T: AllowLabeled,
{
    /// The public constructor used by automatically generated metrics.
    pub fn new(meta: glean_core::CommonMetricData, labels: Option<Vec<String>>) -> Self {
        let submetric = T::new_inner(meta);
        let core = glean_core::metrics::LabeledMetric::new(submetric, labels);
        Self(Arc::new(core))
    }
}

#[inherent(pub)]
impl<T> glean_core::traits::Labeled<T> for LabeledMetric<T>
where
    T: AllowLabeled + Clone,
{
    fn get(&self, label: &str) -> T {
        let inner = self.0.get(label);
        T::from_inner(inner)
    }

    fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>(
        &self,
        error: ErrorType,
        ping_name: S,
    ) -> i32 {
        crate::block_on_dispatcher();

        crate::with_glean_mut(|glean| {
            glean_core::test_get_num_recorded_errors(
                &glean,
                self.0.get_submetric().meta(),
                error,
                ping_name.into(),
            )
            .unwrap_or(0)
        })
    }
}

#[cfg(test)]
mod test {
    use super::ErrorType;
    use crate::common_test::{lock_test, new_glean};
    use crate::destroy_glean;
    use crate::private::{BooleanMetric, CounterMetric, LabeledMetric, StringMetric};
    use crate::CommonMetricData;

    #[test]
    fn test_labeled_counter_type() {
        let _lock = lock_test();

        let _t = new_glean(None, true);

        let metric: LabeledMetric<CounterMetric> = LabeledMetric::new(
            CommonMetricData {
                name: "labeled_counter".into(),
                category: "labeled".into(),
                send_in_pings: vec!["test1".into()],
                ..Default::default()
            },
            None,
        );

        metric.get("label1").add(1);
        metric.get("label2").add(2);
        assert_eq!(1, metric.get("label1").test_get_value("test1").unwrap());
        assert_eq!(2, metric.get("label2").test_get_value("test1").unwrap());
    }

    #[test]
    fn test_other_label_with_predefined_labels() {
        let _lock = lock_test();

        let _t = new_glean(None, true);

        let metric: LabeledMetric<CounterMetric> = LabeledMetric::new(
            CommonMetricData {
                name: "labeled_counter".into(),
                category: "labeled".into(),
                send_in_pings: vec!["test1".into()],
                ..Default::default()
            },
            Some(vec!["foo".into(), "bar".into(), "baz".into()]),
        );

        metric.get("foo").add(1);
        metric.get("foo").add(2);
        metric.get("bar").add(1);
        metric.get("not_there").add(1);
        metric.get("also_not_there").add(1);
        metric.get("not_me").add(1);

        assert_eq!(3, metric.get("foo").test_get_value(None).unwrap());
        assert_eq!(1, metric.get("bar").test_get_value(None).unwrap());
        assert!(metric.get("baz").test_get_value(None).is_none());
        // The rest all lands in the __other__ bucket.
        assert_eq!(3, metric.get("__other__").test_get_value(None).unwrap());
    }

    #[test]
    fn test_other_label_without_predefined_labels() {
        let _lock = lock_test();

        let _t = new_glean(None, true);

        let metric: LabeledMetric<CounterMetric> = LabeledMetric::new(
            CommonMetricData {
                name: "labeled_counter".into(),
                category: "labeled".into(),
                send_in_pings: vec!["test1".into()],
                ..Default::default()
            },
            None,
        );

        // Record in 20 labels: it will go over the maximum number of supported
        // dynamic labels.
        for i in 0..=20 {
            metric.get(format!("label_{}", i).as_str()).add(1);
        }
        // Record in a label once again.
        metric.get("label_0").add(1);

        assert_eq!(2, metric.get("label_0").test_get_value(None).unwrap());
        for i in 1..15 {
            assert_eq!(
                1,
                metric
                    .get(format!("label_{}", i).as_str())
                    .test_get_value(None)
                    .unwrap()
            );
        }
        assert_eq!(5, metric.get("__other__").test_get_value(None).unwrap());
    }

    #[test]
    fn test_other_label_without_predefined_labels_before_glean_init() {
        let _lock = lock_test();

        // We explicitly want Glean to not be initialized.
        destroy_glean(true);

        let metric: LabeledMetric<CounterMetric> = LabeledMetric::new(
            CommonMetricData {
                name: "labeled_counter".into(),
                category: "labeled".into(),
                send_in_pings: vec!["test1".into()],
                ..Default::default()
            },
            None,
        );

        // Record in 20 labels: it will go over the maximum number of supported
        // dynamic labels.
        for i in 0..=20 {
            metric.get(format!("label_{}", i).as_str()).add(1);
        }
        // Record in a label once again.
        metric.get("label_0").add(1);

        // Initialize Glean.
        let _t = new_glean(None, false);

        assert_eq!(2, metric.get("label_0").test_get_value(None).unwrap());
        for i in 1..15 {
            assert_eq!(
                1,
                metric
                    .get(format!("label_{}", i).as_str())
                    .test_get_value(None)
                    .unwrap()
            );
        }
        assert_eq!(5, metric.get("__other__").test_get_value(None).unwrap());
    }

    #[test]
    fn test_labeled_string_type() {
        let _lock = lock_test();

        let _t = new_glean(None, true);

        let metric: LabeledMetric<StringMetric> = LabeledMetric::new(
            CommonMetricData {
                name: "labeled_string".into(),
                category: "labeled".into(),
                send_in_pings: vec!["test1".into()],
                ..Default::default()
            },
            None,
        );

        metric.get("label1").set("foo");
        metric.get("label2").set("bar");
        assert_eq!("foo", metric.get("label1").test_get_value("test1").unwrap());
        assert_eq!("bar", metric.get("label2").test_get_value("test1").unwrap());
    }

    #[test]
    fn test_labeled_boolean_type() {
        let _lock = lock_test();

        let _t = new_glean(None, true);

        let metric: LabeledMetric<BooleanMetric> = LabeledMetric::new(
            CommonMetricData {
                name: "labeled_boolean".into(),
                category: "labeled".into(),
                send_in_pings: vec!["test1".into()],
                ..Default::default()
            },
            None,
        );

        metric.get("label1").set(false);
        metric.get("label2").set(true);
        assert!(!metric.get("label1").test_get_value("test1").unwrap());
        assert!(metric.get("label2").test_get_value("test1").unwrap());
    }

    #[test]
    fn test_invalid_labels_record_errors() {
        let _lock = lock_test();

        let _t = new_glean(None, true);

        let metric: LabeledMetric<BooleanMetric> = LabeledMetric::new(
            CommonMetricData {
                name: "labeled_boolean".into(),
                category: "labeled".into(),
                send_in_pings: vec!["test1".into()],
                ..Default::default()
            },
            None,
        );

        let invalid_label = "!#I'm invalid#--_";
        metric.get(invalid_label).set(true);
        assert!(metric.get("__other__").test_get_value(None).unwrap());
        assert_eq!(
            1,
            metric.test_get_num_recorded_errors(ErrorType::InvalidLabel, None)
        );
    }
}
