use itertools::Itertools;
use log::{info, warn};
use std::collections::BTreeSet as Set;
use tokio::process::Command;

use super::{Result, format_err, retry_or_ignore};
use crate::{
    udev,
    utils::{NameAliases, select_n_by_regex, select_n_by_udev},
};
use dkl::bootstrap::{Config, Network};

pub async fn setup(cfg: &Config) {
    if cfg.networks.is_empty() {
        warn!("no networks configured");
        return;
    }

    let mut assigned = Set::new();

    for net in &cfg.networks {
        retry_or_ignore(async || setup_network(net, &mut assigned).await).await;
    }
}

async fn setup_network(net: &Network, assigned: &mut Set<String>) -> Result<()> {
    info!("setting up network {}", net.name);

    let netdevs = (get_interfaces().await?)
        .filter(|dev| !assigned.contains(dev.name()))
        .collect::<Vec<_>>();

    for dev in &netdevs {
        let names = [dev.name()].into_iter().chain(dev.aliases()).join(", ");
        info!("- available network device: {}", names);
    }

    let mut cmd = Command::new("ash");
    cmd.arg("-c");
    cmd.arg(&net.script);

    let mut selected = Vec::new();

    for iface in &net.interfaces {
        let var = &iface.var;

        let if_names = if let Some(ref udev_filter) = iface.udev {
            select_n_by_udev(
                iface.n,
                "net",
                "INTERFACE",
                &udev_filter.clone().into(),
                assigned,
            )
            .await?
        } else {
            let netdevs = netdevs.iter().filter(|na| !assigned.contains(na.name()));
            select_n_by_regex(iface.n, &iface.regexps, netdevs)
        };

        if if_names.is_empty() {
            return Err(format_err!("- no interface match for {var:?}"));
        }

        let value = if_names.join(" ");
        info!("- {var}={value}");
        cmd.env(var, value);

        selected.extend(if_names);
    }

    info!("- running script");
    let status = cmd.status().await?;
    if !status.success() {
        return Err(format_err!("setup script failed: {status}"));
    }

    assigned.extend(selected);
    Ok(())
}

async fn get_interfaces() -> Result<impl Iterator<Item = NameAliases>> {
    let nas: Vec<_> = (udev::all().await?.of_subsystem("net"))
        .filter_map(|dev| {
            let name = dev.property("INTERFACE")?;
            let mut na = NameAliases::new(name.to_string());

            for (p, v) in dev.properties() {
                if p.starts_with("ID_NET_NAME") {
                    na.push(v.to_string());
                }
            }

            Some(na)
        })
        .collect();
    Ok(nas.into_iter())
}
