use eyre::{Result, format_err};
use log::{error, info, warn};
use std::collections::BTreeSet as Set;
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tokio::sync::Mutex;

use super::{USED_DEVS, retry_or_ignore};
use crate::blockdev::{is_uninitialized, uninitialize};
use crate::fs::walk_dir;
use crate::input;
use dkl::bootstrap::{CryptDev, DevFilter};

pub async fn setup(devs: &[CryptDev]) {
    if devs.is_empty() {
        return;
    }

    let mut used_devs = USED_DEVS.lock().await;

    // CryptDev.name that have a least one assignment done
    let mut done = Set::new();

    // dmcrypt devices opened here
    let mut done_crypt = Set::new();

    retry_or_ignore(async || {
        let all_devs = walk_dir("/dev").await;

        for dev in devs {
            let mut mappings = find_dev(dev, &all_devs).await?;
            mappings.retain(|(_, dev_path)| !used_devs.contains(dev_path));

            if mappings.is_empty() && !dev.optional() && !done.contains(&dev.name) {
                return Err(format_err!("no device found for crypt dev {}", dev.name));
            }

            for (crypt_dev, dev_path) in mappings {
                if done_crypt.contains(&crypt_dev) {
                    continue;
                }

                info!("crypt dev {crypt_dev}: using {dev_path}");

                crypt_open(&crypt_dev, &dev_path).await?;

                done_crypt.insert(crypt_dev);
                used_devs.insert(dev_path);
                done.insert(&dev.name);
            }
        }

        Ok(())
    })
    .await;
}

struct PrevPw {
    pw: String,
    reuse: bool,
}

impl PrevPw {
    fn is_set(&self) -> bool {
        !self.pw.is_empty()
    }

    fn can_reuse(&self) -> bool {
        self.reuse && self.is_set()
    }

    fn invalidate(&mut self) {
        self.pw = String::new();
        self.reuse = false;
    }

    async fn input(&mut self, prompt: impl std::fmt::Display) -> String {
        if self.can_reuse() {
            info!("reusing password");
            self.pw.clone()
        } else if self.is_set() {
            let pw =
                input::read_password(format!("{prompt} (\"\" reuse, \"*\" auto-reuse)? ")).await;

            match pw.as_str() {
                "" => self.pw.clone(),
                "*" => {
                    self.reuse = true;
                    self.pw.clone()
                }
                _ => {
                    self.pw = pw.clone();
                    pw
                }
            }
        } else {
            let pw = loop {
                let pw = input::read_password(format!("{prompt}? ")).await;
                if pw.is_empty() {
                    error!("empty password provided!");
                    continue;
                }
                break pw;
            };

            self.pw = pw.clone();
            pw
        }
    }
}

static PREV_PW: Mutex<PrevPw> = Mutex::const_new(PrevPw {
    pw: String::new(),
    reuse: false,
});

async fn crypt_open(crypt_dev: &str, dev_path: &str) -> Result<()> {
    'open_loop: loop {
        let mut prev_pw = PREV_PW.lock().await;

        let pw = prev_pw
            .input(format!("crypt password for {crypt_dev}"))
            .await;

        if cryptsetup(&pw, ["open", dev_path, crypt_dev]).await? {
            return Ok(());
        }

        error!("crypt open {crypt_dev} from {dev_path} failed");

        if is_uninitialized(dev_path).await? {
            // we can format the device
            info!("{dev_path} looks uninitialized, it may be formatted");

            match input::read_choice(["[f]ormat", "[r]etry", "[i]gnore"]).await {
                'r' => continue 'open_loop,
                'i' => return Ok(()),
                'f' => {
                    if !cryptsetup(&pw, ["luksFormat", dev_path]).await? {
                        return Err(format_err!("cryptsetup luksFormat failed"));
                    }
                    if !cryptsetup(&pw, ["open", dev_path, crypt_dev]).await? {
                        return Err(format_err!("open after format failed"));
                    }

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

                    return Ok(());
                }
                _ => unreachable!(),
            }
        }

        // device looks initialized, don't allow format
        warn!("{dev_path} looks initialized, formatting not allowed from init");

        prev_pw.invalidate();

        match input::read_choice(["[r]etry", "[i]gnore"]).await {
            'r' => continue 'open_loop,
            'i' => return Ok(()),
            _ => unreachable!(),
        }
    }
}

async fn cryptsetup<const N: usize>(pw: &str, args: [&str; N]) -> Result<bool> {
    let mut child = Command::new("cryptsetup")
        .args(args)
        .arg("--key-file=-")
        .stdin(Stdio::piped())
        .spawn()?;

    (child.stdin.as_mut().unwrap())
        .write_all(pw.as_bytes())
        .await?;

    Ok(child.wait().await?.success())
}

async fn find_dev(dev: &CryptDev, all_devs: &[String]) -> Result<Vec<(String, String)>> {
    let dev_name = &dev.name;
    Ok(match dev.filter() {
        DevFilter::None => vec![],
        DevFilter::Dev(path) => (all_devs.iter())
            .filter(|dev_path| dev_path == &path)
            .map(|dev_path| (dev.name.clone(), dev_path.clone()))
            .collect(),
        DevFilter::Prefix(prefix) => (all_devs.iter())
            .filter_map(|path| {
                let suffix = path.strip_prefix(prefix)?;
                Some((format!("{dev_name}{suffix}"), path.clone()))
            })
            .collect(),
        DevFilter::Udev(filter) => {
            use crate::udev;
            let devs = udev::all().await?;

            let filter: udev::Filter = filter.clone().into();

            (devs.iter())
                .filter(|dev| dev.subsystem() == Some("block") && filter.matches(dev))
                .filter_map(|dev| {
                    let path = dev.property("DEVNAME")?.to_string();

                    let mut name = dev_name.replace("${name}", dev.name()?);
                    for (p, v) in dev.properties() {
                        name = name.replace(&format!("${{{p}}}"), v);
                    }

                    Some((name, path))
                })
                .collect()
        }
    })
}
