use log::error;
use std::collections::BTreeSet as Set;
use std::sync::LazyLock;

use crate::udev;

static CMDLINE: LazyLock<String> = LazyLock::new(|| {
    std::fs::read("/proc/cmdline")
        .inspect_err(|e| error!("failed to read kernel cmdline: {e}"))
        .map(|c| String::from_utf8_lossy(&c).to_string())
        .unwrap_or_default()
});

pub fn cmdline() -> impl Iterator<Item = &'static str> {
    CMDLINE.split_ascii_whitespace()
}

pub fn bool_param(name: &str) -> bool {
    let name1 = &format!("dkl.{name}");
    let name2 = &format!("direktil.{name}");

    cmdline().any(|part| part == name1 || part == name2)
}

pub fn param(name: &str) -> Option<&'static str> {
    let prefix1 = &format!("dkl.{name}=");
    let prefix2 = &format!("direktil.{name}=");

    cmdline().find_map(|part| {
        part.strip_prefix(prefix1)
            .or_else(|| part.strip_prefix(prefix2))
    })
}

pub struct NameAliases {
    name: String,
    aliases: Set<String>,
}

impl NameAliases {
    pub fn new(name: String) -> Self {
        Self {
            name,
            aliases: Set::new(),
        }
    }

    pub fn name(&self) -> &str {
        self.name.as_str()
    }
    pub fn aliases(&self) -> impl Iterator<Item = &str> {
        self.aliases.iter().map(|s| s.as_str())
    }

    pub fn iter(&self) -> impl Iterator<Item = &str> {
        std::iter::once(self.name()).chain(self.aliases())
    }

    pub fn push(&mut self, alias: String) {
        if self.name == alias {
            return;
        }
        self.aliases.insert(alias);
    }
}

pub fn select_n_by_regex<'t>(
    n: i16,
    regexs: &[String],
    nas: impl Iterator<Item = &'t NameAliases>,
) -> Vec<String> {
    // compile regexs
    let regexs: Vec<_> = (regexs.iter())
        .filter_map(|re| {
            regex::Regex::new(re)
                .inspect_err(|e| error!("invalid regex ignored: {re:?}: {e}"))
                .ok()
        })
        .collect();

    let matching = |name| regexs.iter().any(|re| re.is_match(name));

    let nas = nas
        .filter(|na| na.iter().any(matching))
        .map(|na| na.name().to_string());

    if n == -1 {
        nas.collect()
    } else {
        nas.take(n as usize).collect()
    }
}

pub async fn select_n_by_udev(
    n: i16,
    subsystem: &str,
    result_property: &str,
    filter: &udev::Filter,
    in_use: &Set<String>,
) -> eyre::Result<Vec<String>> {
    let devs = udev::all().await?;
    let nas = devs
        .of_subsystem(subsystem)
        .filter(|dev| filter.matches(dev))
        .filter_map(|dev| Some(dev.property(result_property)?.to_string()))
        .filter(|name| !in_use.contains(name));

    Ok(if n == -1 {
        nas.collect()
    } else {
        nas.take(n as usize).collect()
    })
}
