use eyre::{format_err, Result};
use tokio::process::Command;

#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Report {
    report: Vec<ReportObj>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
enum ReportObj {
    #[serde(rename = "pv")]
    PV(Vec<PV>),
    #[serde(rename = "vg")]
    VG(Vec<VG>),
    #[serde(rename = "lv")]
    LV(Vec<LV>),
}

#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct PV {
    pub pv_name: String,
    pub vg_name: String,
    pub pv_fmt: String,
    pub pv_attr: String,
    pub pv_size: String,
    pub pv_free: String,
}

#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct VG {
    pub vg_name: String,
    pub pv_count: String,
    pub lv_count: String,
    pub snap_count: String,
    pub vg_attr: String,
    pub vg_size: String,
    pub vg_free: String,
}

#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct LV {
    pub lv_name: String,
    pub vg_name: String,
    pub lv_attr: String,
    pub lv_size: String,
    pub pool_lv: String,
    pub origin: String,
    pub data_percent: String,
    pub metadata_percent: String,
    pub move_pv: String,
    pub mirror_log: String,
    pub copy_percent: String,
    pub convert_lv: String,
}

impl LV {
    pub fn equal_name(&self, vg_name: &str, lv_name: &str) -> bool {
        vg_name == self.vg_name && lv_name == self.lv_name
    }
}

pub async fn pvs() -> Result<Vec<PV>> {
    report_cmd("pvs", |o| match o {
        ReportObj::PV(pv) => Some(pv),
        _ => None,
    })
    .await
}

pub async fn vgs() -> Result<Vec<VG>> {
    report_cmd("vgs", |o| match o {
        ReportObj::VG(vg) => Some(vg),
        _ => None,
    })
    .await
}

pub async fn lvs() -> Result<Vec<LV>> {
    report_cmd("lvs", |o| match o {
        ReportObj::LV(lv) => Some(lv),
        _ => None,
    })
    .await
}

async fn report_cmd<T>(cmd: &str, find: fn(ReportObj) -> Option<Vec<T>>) -> Result<Vec<T>> {
    let output = Command::new(cmd)
        .arg("--reportformat=json")
        .output()
        .await?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(output.stderr.trim_ascii());
        return Err(format_err!("{cmd} failed: {}", stderr));
    }

    let report: Report = serde_json::from_slice(&output.stdout).unwrap();
    Ok((report.report.into_iter())
        .filter_map(find)
        .flatten()
        .collect())
}
