use eyre::{Result, format_err};
use log::{error, info, warn};
use tokio::process::Command;

use super::{USED_DEVS, exec, retry, retry_or_ignore};
use crate::fs::walk_dir;
use crate::{blockdev, lvm};
use dkl::bootstrap::{Config, Filesystem, LvSize, LvmLV, LvmPV, LvmVG, TAKE_ALL};

pub async fn setup(cfg: &Config) {
    if cfg.lvm.is_empty() {
        info!("no LVM VG configured");
        return;
    }

    exec("pvscan", &[]).await;
    exec("vgscan", &["--mknodes"]).await;

    for vg in &cfg.lvm {
        retry_or_ignore(async || setup_vg(vg).await).await
    }

    let lvs = retry(lvm::lvs).await;

    for vg in &cfg.lvm {
        let vg_name = vg.name.as_str();

        for lv in &vg.lvs {
            let lv_name = lv.name.as_str();

            if (lvs.iter()).any(|lv| lv.equal_name(vg_name, lv_name)) {
                info!("LVM LV {vg_name}/{lv_name} exists");
            } else {
                retry_or_ignore(async || setup_lv(vg, lv).await).await;
            }
        }
    }

    exec("vgchange", &["--sysinit", "-a", "ly"]).await;

    for vg in &cfg.lvm {
        for lv in &vg.lvs {
            retry_or_ignore(async || format_lv(vg, lv).await).await;
        }
    }
}

async fn setup_vg(vg: &LvmVG) -> Result<()> {
    let vg_name = vg.name.as_str();

    let pvs = retry(lvm::pvs).await;

    let mut dev_done = pvs.iter().filter(|pv| pv.vg_name == vg.name).count();
    let dev_needed = vg.pvs.n;
    macro_rules! missing_count {
        () => {
            (dev_needed as usize) - dev_done
        };
    }

    if dev_needed == TAKE_ALL {
        if dev_done == 0 {
            info!("setting up LVM VG {vg_name} using all matching devices");
        } else {
            // in "take all" mode, don't extend as existing vg at boot
            info!("LVM VG {vg_name} exists");
            return Ok(());
        }
    } else if dev_done >= (dev_needed as usize) {
        info!("LVM VG {vg_name} exists with enough devices");
        return Ok(()); // already set up
    } else {
        info!("setting up LVM VG {vg_name} ({dev_done}/{dev_needed} devices configured)");
    }

    let matching_devs = find_devs(&vg.pvs).await?;

    let devs: Vec<_> = if dev_needed == TAKE_ALL {
        matching_devs
    } else {
        matching_devs.into_iter().take(missing_count!()).collect()
    };

    let cmd = if dev_done == 0 {
        if devs.is_empty() {
            return Err(format_err!("creating but no devices found"));
        }
        "vgcreate"
    } else {
        "vgextend"
    };
    let status = (Command::new(cmd).arg(vg_name).args(&devs))
        .status()
        .await?;
    if !status.success() {
        return Err(format_err!("{cmd} failed: {status}"));
    }

    dev_done += devs.len();
    USED_DEVS.lock().await.extend(devs);

    if dev_needed != TAKE_ALL && dev_done < (dev_needed as usize) {
        return Err(format_err!(
            "LVM VG {vg_name} needs {} more device(s)",
            missing_count!()
        ));
    }

    Ok(())
}

async fn setup_lv(vg: &LvmVG, lv: &LvmLV) -> Result<()> {
    let name = format!("{}/{}", vg.name, lv.name);
    info!("creating LV {name}");

    let mut cmd = Command::new("lvcreate");
    cmd.arg(&vg.name);
    cmd.args(["--name", &lv.name]);

    match &lv.size {
        LvSize::Size(sz) => cmd.args(["-L", sz]),
        LvSize::Extents(sz) => cmd.args(["-l", sz]),
    };

    let raid = lv.raid.as_ref().unwrap_or(&vg.defaults.raid);

    if let Some(mirrors) = raid.mirrors {
        cmd.args(["--mirrors", &mirrors.to_string()]);
    }
    if let Some(stripes) = raid.stripes {
        cmd.args(["--stripes", &stripes.to_string()]);
    }

    let status = cmd.status().await?;
    if !status.success() {
        return Err(format_err!("lvcreate failed: {status}"));
    }

    if let Err(e) = blockdev::uninitialize(&format!("/dev/{name}")).await {
        warn!("uninitialize failed (ignored): {e}");
    }

    Ok(())
}

async fn format_lv(vg: &LvmVG, lv: &LvmLV) -> Result<()> {
    let name = &format!("{}/{}", vg.name, lv.name);
    let dev = &format!("/dev/{name}");

    if !blockdev::is_uninitialized(dev).await? {
        info!("{dev} looks initialized");
        return Ok(());
    }

    let fs = lv.fs.as_ref().unwrap_or(&vg.defaults.fs);
    info!("initializing {} filesystem on {dev}", fs.fstype());

    let mkfs = format!("mkfs.{}", fs.fstype());

    let mut cmd = Command::new(&mkfs);

    // filesystem specific flags
    match fs {
        Filesystem::Ext4 => {
            cmd.arg("-F");
        }
        Filesystem::Btrfs | Filesystem::Xfs => {
            cmd.arg("-f");
        }
        &Filesystem::Other(_) => {}
    }

    cmd.arg(dev);

    let mut child = match cmd.spawn() {
        Ok(v) => v,
        Err(e) => {
            // try simple fixes
            match fs {
                Filesystem::Xfs => install_package("xfsprogs").await?,
                Filesystem::Btrfs => install_package("btrs-progs").await?,
                _ => Err(format_err!("{mkfs} failed: {e}"))?,
            }
            cmd.spawn().map_err(|e| format_err!("{mkfs} failed: {e}"))?
        }
    };

    let status = child.wait().await?;
    if !status.success() {
        return Err(format_err!("{mkfs} failed: {status}"));
    }

    Ok(())
}

async fn install_package(pkg: &str) -> Result<()> {
    let status = Command::new("apk").arg("add").arg(pkg).status().await?;
    if status.success() {
        Ok(())
    } else {
        Err(format_err!("failed to install package {pkg}: {status}"))
    }
}

async fn find_devs(pvs: &LvmPV) -> Result<Vec<String>> {
    let mut results = if let Some(ref filter) = pvs.udev {
        use crate::udev;
        let filter: udev::Filter = filter.clone().into();
        (udev::all().await?.iter())
            .filter(|dev| dev.subsystem() == Some("block") && filter.matches(dev))
            .filter_map(|dev| dev.property("DEVNAME").map(|s| s.to_string()))
            .collect()
    } else if !pvs.regexps.is_empty() {
        let regexps: Vec<regex::Regex> = (pvs.regexps.iter())
            .filter_map(|re_str| {
                (re_str.parse())
                    .inspect_err(|e| error!("invalid regex ignored: {re_str:?}: {e}"))
                    .ok()
            })
            .collect();

        (walk_dir("/dev").await.into_iter())
            .filter(|path| regexps.iter().any(|re| re.is_match(path)))
            .collect()
    } else {
        warn!("no device filters, no matches");
        vec![]
    };

    let used_devs = USED_DEVS.lock().await;
    results.retain(|path| !used_devs.contains(path.as_str()));
    Ok(results)
}
