use {
    rayon::prelude::*,
    serde::{Deserialize, Serialize},
    sha2::{digest::Digest, Sha256},
    std::{
        error::Error,
        fmt::Write,
        fs,
        io::Read,
        os::unix::fs::MetadataExt,
        path::{Path, PathBuf},
    },
    walkdir::WalkDir,
};

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Plist {
    pub entries: Vec<Entry>,
}

impl TryFrom<&Path> for Plist {
    type Error = Box<dyn Error>;

    fn try_from(value: &Path) -> Result<Self, Self::Error> {
        let entries = WalkDir::new(value)
            .into_iter()
            .collect::<Vec<_>>()
            .par_iter()
            .filter(|x| x.is_ok())
            .filter_map(|x| {
                Entry::try_from(x.as_ref().unwrap().path().to_path_buf().as_path()).ok()
            })
            .collect();
        Ok(Self { entries })
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Entry {
    File {
        path: PathBuf,
        sha256sum: String,
        mode: u32,
        size: usize,
    },
    Directory {
        path: PathBuf,
        mode: u32,
    },
    Link {
        path: PathBuf,
        target: PathBuf,
    },
}

impl TryFrom<&Path> for Entry {
    type Error = Box<dyn Error>;

    fn try_from(value: &Path) -> Result<Self, Self::Error> {
        let mut path = PathBuf::from("/");
        path.push(value);
        let meta = fs::metadata(value)?;
        if meta.is_file() {
            let mut buf = vec![];
            let mut fd = fs::File::open(value)?;
            let size = fd.read_to_end(&mut buf)?;
            let mut sha256sum = String::new();
            let mut hasher = Sha256::new();
            hasher.update(&buf);
            let res = hasher.finalize();
            for c in res {
                write!(sha256sum, "{c:02x}")?;
            }
            let mode = meta.mode();
            Ok(Self::File {
                path,
                sha256sum,
                mode,
                size,
            })
        } else if meta.is_dir() {
            let mode = meta.mode();
            Ok(Self::Directory { path, mode })
        } else if meta.is_symlink() {
            let target = fs::read_link(value)?;
            Ok(Self::Link { path, target })
        } else {
            unreachable!();
        }
    }
}
