use serde::{Deserialize, Serialize};
#[cfg(not(target_os = "windows"))]
use std::cmp;

#[cfg(not(target_os = "windows"))]
use log::*;
#[cfg(not(target_os = "windows"))]
use std::{ffi::CString, mem::zeroed};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DiskVariant {
    MountPointOnly(String),
    Options(DiskOptions),
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DiskOptions {
    mount: String,
    #[serde(default, rename = "type")]
    disk_type: DiskType,
    #[serde(default)]
    output_type: OutputType,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum DiskType {
    Size,
    Used,
    #[default]
    Free,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum OutputType {
    #[default]
    Bytes,
    Human,
    Percent,
}

impl DiskVariant {
    pub async fn run(&self) -> Result<String, String> {
        let diskops = match *self {
            DiskVariant::MountPointOnly(ref mount) => DiskOptions {
                mount: mount.clone(),
                disk_type: DiskType::Free,
                output_type: OutputType::Bytes,
            },
            DiskVariant::Options(ref ops) => ops.clone(),
        };

        let stavfs = get_stats(&diskops)?;

        Ok(stavfs)
    }
}

#[cfg(not(target_os = "windows"))]
pub fn get_stats(ops: &DiskOptions) -> Result<String, String> {
    let mountp = CString::new(ops.mount.clone()).unwrap();
    let mnt_ptr = mountp.as_ptr();

    let stats = unsafe {
        let mut stats: libc::statvfs = zeroed();
        if libc::statvfs(mnt_ptr, &mut stats) != 0 {
            return Err(format!(
                "Unable to retrive stats of {}: {}",
                ops.mount,
                std::io::Error::last_os_error()
            ));
        }

        stats
    };

    debug!(
        "f_blocks:{}, f_bsize:{}, f_frsize:{}, f_bavail:{}, f_bfree:{}",
        stats.f_blocks, stats.f_bsize, stats.f_frsize, stats.f_bavail, stats.f_bfree
    );

    let size = stats.f_blocks as usize * stats.f_frsize as usize;
    let free = stats.f_bavail as usize * stats.f_frsize as usize;
    let used = size - free;

    debug!("size: {}, free:{}, used:{}", size, free, used);

    let output = match ops.disk_type {
        DiskType::Size => size,
        DiskType::Used => used,
        DiskType::Free => free,
    };

    match ops.output_type {
        OutputType::Bytes => Ok(output.to_string()),
        OutputType::Percent => {
            if size == 0 {
                return Err(format!(
                    "Size for mount `{}` is 0.  Can't create percentage",
                    ops.mount
                ));
            }
            Ok(format!(
                "{}%",
                ((output as f64 / size as f64) * 100.0).round() as usize
            ))
        }
        OutputType::Human => Ok(pretty_bytes(output as f64)),
    }
}

#[cfg(target_os = "windows")]
pub fn get_stats(_ops: &DiskOptions) -> Result<String, String> {
    return Err("Not Implemented Yet".into());
}

#[cfg(not(target_os = "windows"))]
pub fn pretty_bytes(num: f64) -> String {
    let negative = if num.is_sign_positive() { "" } else { "-" };
    let num = num.abs();
    let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
    if num < 1_f64 {
        return format!("{}{} {}", negative, num, "B");
    }
    let delimiter = 1000_f64;
    let exponent = cmp::min(
        (num.ln() / delimiter.ln()).floor() as i32,
        (units.len() - 1) as i32,
    );

    let unit = units[exponent as usize];
    format!("{}{:.2}{}", negative, num / delimiter.powi(exponent), unit)
}
