use futures::StreamExt;
use heim::units::information::byte;
#[cfg(not(windows))]
use heim::units::ratio::ratio;
use vector_lib::configurable::configurable_component;
use vector_lib::metric_tags;
use crate::internal_events::{HostMetricsScrapeDetailError, HostMetricsScrapeFilesystemError};
use super::{default_all_devices, example_devices, filter_result, FilterList, HostMetrics};
#[configurable_component]
#[derive(Clone, Debug, Default)]
pub struct FilesystemConfig {
    #[serde(default = "default_all_devices")]
    #[configurable(metadata(docs::examples = "example_devices()"))]
    devices: FilterList,
    #[serde(default = "default_all_devices")]
    #[configurable(metadata(docs::examples = "example_filesystems()"))]
    filesystems: FilterList,
    #[serde(default = "default_all_devices")]
    #[configurable(metadata(docs::examples = "example_mountpoints()"))]
    mountpoints: FilterList,
}
fn example_filesystems() -> FilterList {
    FilterList {
        includes: Some(vec!["ntfs".try_into().unwrap()]),
        excludes: Some(vec!["ext*".try_into().unwrap()]),
    }
}
fn example_mountpoints() -> FilterList {
    FilterList {
        includes: Some(vec!["/home".try_into().unwrap()]),
        excludes: Some(vec!["/raid*".try_into().unwrap()]),
    }
}
impl HostMetrics {
    pub async fn filesystem_metrics(&self, output: &mut super::MetricsBuffer) {
        output.name = "filesystem";
        match heim::disk::partitions().await {
            Ok(partitions) => {
                for (partition, usage) in partitions
                    .filter_map(|result| {
                        filter_result(result, "Failed to load/parse partition data.")
                    })
                    .map(|partition| {
                        self.config
                            .filesystem
                            .mountpoints
                            .contains_path(Some(partition.mount_point()))
                            .then_some(partition)
                    })
                    .filter_map(|partition| async { partition })
                    .map(|partition| {
                        self.config
                            .filesystem
                            .devices
                            .contains_path(partition.device().map(|d| d.as_ref()))
                            .then_some(partition)
                    })
                    .filter_map(|partition| async { partition })
                    .map(|partition| {
                        self.config
                            .filesystem
                            .filesystems
                            .contains_str(Some(partition.file_system().as_str()))
                            .then_some(partition)
                    })
                    .filter_map(|partition| async { partition })
                    .filter_map(|partition| async {
                        heim::disk::usage(partition.mount_point())
                            .await
                            .map_err(|error| {
                                emit!(HostMetricsScrapeFilesystemError {
                                    message: "Failed to load partitions info.",
                                    mount_point: partition
                                        .mount_point()
                                        .to_str()
                                        .unwrap_or("unknown")
                                        .to_string(),
                                    error,
                                })
                            })
                            .map(|usage| (partition, usage))
                            .ok()
                    })
                    .collect::<Vec<_>>()
                    .await
                {
                    let fs = partition.file_system();
                    let mut tags = metric_tags! {
                        "filesystem" => fs.as_str(),
                        "mountpoint" => partition.mount_point().to_string_lossy()
                    };
                    if let Some(device) = partition.device() {
                        tags.replace("device".into(), device.to_string_lossy().to_string());
                    }
                    output.gauge(
                        "filesystem_free_bytes",
                        usage.free().get::<byte>() as f64,
                        tags.clone(),
                    );
                    output.gauge(
                        "filesystem_total_bytes",
                        usage.total().get::<byte>() as f64,
                        tags.clone(),
                    );
                    output.gauge(
                        "filesystem_used_bytes",
                        usage.used().get::<byte>() as f64,
                        tags.clone(),
                    );
                    #[cfg(not(windows))]
                    output.gauge(
                        "filesystem_used_ratio",
                        usage.ratio().get::<ratio>() as f64,
                        tags,
                    );
                }
            }
            Err(error) => {
                emit!(HostMetricsScrapeDetailError {
                    message: "Failed to load partitions info.",
                    error,
                });
            }
        }
    }
}
#[cfg(test)]
mod tests {
    use super::{
        super::{
            tests::{all_gauges, assert_filtered_metrics, count_name, count_tag},
            HostMetrics, HostMetricsConfig, MetricsBuffer,
        },
        FilesystemConfig,
    };
    #[cfg(not(windows))]
    #[tokio::test]
    async fn generates_filesystem_metrics() {
        let mut buffer = MetricsBuffer::new(None);
        HostMetrics::new(HostMetricsConfig::default())
            .filesystem_metrics(&mut buffer)
            .await;
        let metrics = buffer.metrics;
        assert!(!metrics.is_empty());
        assert!(metrics.len() % 4 == 0);
        assert!(all_gauges(&metrics));
        for name in &[
            "filesystem_free_bytes",
            "filesystem_total_bytes",
            "filesystem_used_bytes",
            "filesystem_used_ratio",
        ] {
            assert_eq!(
                count_name(&metrics, name),
                metrics.len() / 4,
                "name={}",
                name
            );
        }
        assert_eq!(count_tag(&metrics, "filesystem"), metrics.len());
        assert_eq!(count_tag(&metrics, "mountpoint"), metrics.len());
    }
    #[cfg(windows)]
    #[tokio::test]
    async fn generates_filesystem_metrics() {
        let mut buffer = MetricsBuffer::new(None);
        HostMetrics::new(HostMetricsConfig::default())
            .filesystem_metrics(&mut buffer)
            .await;
        let metrics = buffer.metrics;
        assert!(!metrics.is_empty());
        assert!(metrics.len() % 3 == 0);
        assert!(all_gauges(&metrics));
        for name in &[
            "filesystem_free_bytes",
            "filesystem_total_bytes",
            "filesystem_used_bytes",
        ] {
            assert_eq!(
                count_name(&metrics, name),
                metrics.len() / 3,
                "name={}",
                name
            );
        }
        assert_eq!(count_tag(&metrics, "filesystem"), metrics.len());
        assert_eq!(count_tag(&metrics, "mountpoint"), metrics.len());
    }
    #[tokio::test]
    async fn filesystem_metrics_filters_on_device() {
        assert_filtered_metrics("device", |devices| async move {
            let mut buffer = MetricsBuffer::new(None);
            HostMetrics::new(HostMetricsConfig {
                filesystem: FilesystemConfig {
                    devices,
                    ..Default::default()
                },
                ..Default::default()
            })
            .filesystem_metrics(&mut buffer)
            .await;
            buffer.metrics
        })
        .await;
    }
    #[tokio::test]
    async fn filesystem_metrics_filters_on_filesystem() {
        assert_filtered_metrics("filesystem", |filesystems| async move {
            let mut buffer = MetricsBuffer::new(None);
            HostMetrics::new(HostMetricsConfig {
                filesystem: FilesystemConfig {
                    filesystems,
                    ..Default::default()
                },
                ..Default::default()
            })
            .filesystem_metrics(&mut buffer)
            .await;
            buffer.metrics
        })
        .await;
    }
    #[tokio::test]
    async fn filesystem_metrics_filters_on_mountpoint() {
        assert_filtered_metrics("mountpoint", |mountpoints| async move {
            let mut buffer = MetricsBuffer::new(None);
            HostMetrics::new(HostMetricsConfig {
                filesystem: FilesystemConfig {
                    mountpoints,
                    ..Default::default()
                },
                ..Default::default()
            })
            .filesystem_metrics(&mut buffer)
            .await;
            buffer.metrics
        })
        .await;
    }
}