use {
    crate::{tar, Entry, Item, ItemError, Package, Plist, Specs},
    deku::DekuError,
    rayon::prelude::{IntoParallelRefIterator, ParallelIterator},
    std::{
        borrow::BorrowMut,
        env,
        error::Error,
        fmt,
        fs::{self, File},
        io::{self, Write},
        path::{Path, PathBuf},
        sync::{
            atomic::{AtomicUsize, Ordering},
            mpsc::Sender,
            Mutex,
        },
    },
    walkdir::WalkDir,
    zstd::Encoder,
};

#[derive(Debug, Clone)]
pub enum Message {
    MemberAdded(String),
    Success(String),
}

/// Creates a package archive
pub struct Creator {
    path: PathBuf,
    entries: Vec<PathBuf>,
    specs: Specs,
}

impl Creator {
    #[allow(clippy::missing_panics_doc)]
    pub fn new(path: &Path, specs: Specs) -> Result<Self, io::Error> {
        let d = env::current_dir()?;
        env::set_current_dir(path)?;
        let path = path.to_path_buf();
        let entries = WalkDir::new(".")
            .into_iter()
            .filter(Result::is_ok)
            .map(|x| x.unwrap().path().to_path_buf())
            .filter(|x| !x.is_dir())
            .collect::<Vec<_>>();
        env::set_current_dir(d)?;
        Ok(Self {
            path,
            entries,
            specs,
        })
    }

    pub fn from_list(list: &[&str], path: Option<&Path>, specs: Specs) -> Result<Self, io::Error> {
        let entries = list.iter().map(|x| Path::new(x).to_path_buf()).collect();
        let path = if let Some(p) = path {
            p.to_path_buf()
        } else {
            env::current_dir().unwrap_or(Path::new("/").to_path_buf())
        };
        Ok(Self {
            path,
            entries,
            specs,
        })
    }

    pub fn len(&self) -> usize {
        self.entries.len()
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn create(self, outdir: &Path, sender: &Sender<Message>) -> Result<(), Box<dyn Error>> {
        let d = env::current_dir()?;
        let plist = Mutex::new(Plist::default());
        let totalsize: AtomicUsize = 0.into();
        let fullname = format!(
            "{}-{}_{}",
            &self.specs.name, self.specs.version, self.specs.release
        );
        if !outdir.exists() {
            fs::create_dir_all(outdir)?;
        }
        let outdir = outdir.canonicalize()?;
        let mut archive = outdir.clone();
        archive.push(&fullname);
        archive.set_extension("tar.zst");
        let fd = File::create(&archive)?;
        let writer = Mutex::new(Encoder::new(fd, 0)?);
        env::set_current_dir(&self.path)?;
        let s = sender.clone();
        self.entries
            .par_iter()
            .filter(|x| x.as_path() != Path::new("."))
            .try_for_each_with(s, |s, x| {
                let item = Item::try_create(x.as_path())?;
                if let Entry::File {
                    path: _,
                    sha256sum: _,
                    mode: _,
                    size,
                } = &item.entry
                {
                    totalsize.fetch_add(*size, Ordering::Release);
                }
                let path = match item.entry.clone() {
                    Entry::File {
                        path,
                        sha256sum: _,
                        mode: _,
                        size: _,
                    }
                    | Entry::Link { path, target: _ }
                    | Entry::Directory { path, mode: _ } => path,
                };
                plist.lock().unwrap().borrow_mut().entries.push(item.entry);
                writer.lock().unwrap().borrow_mut().write_all(&item.data)?;
                s.send(Message::MemberAdded(format!("{}", path.display())))
                    .expect("couldn't send message");
                Ok::<(), CreationError>(())
            })?;
        let mut package: Package = self.specs.into();
        package.size = totalsize.into_inner();
        let plist = plist.into_inner()?;
        package.plist = plist;
        let node = package.save_ron_and_create_tar_node(&outdir)?;
        let mut writer = writer.into_inner()?;
        writer.write_all(&node.to_vec()?)?;
        let _fd = writer.finish()?;
        sender.send(Message::Success(format!("{} saved", archive.display())))?;
        env::set_current_dir(d)?;
        Ok(())
    }
}

#[derive(Debug)]
pub enum CreationError {
    Io(io::Error),
    Fmt(fmt::Error),
    Tar(tar::Error),
    Deku(DekuError),
}

impl fmt::Display for CreationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{self:?}")
    }
}

impl Error for CreationError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Io(e) => Some(e),
            Self::Fmt(e) => Some(e),
            Self::Tar(e) => Some(e),
            Self::Deku(e) => Some(e),
        }
    }
}

impl From<io::Error> for CreationError {
    fn from(value: io::Error) -> Self {
        Self::Io(value)
    }
}

impl From<ItemError> for CreationError {
    fn from(value: ItemError) -> Self {
        match value {
            ItemError::Io(e) => Self::Io(e),
            ItemError::Fmt(e) => Self::Fmt(e),
            ItemError::Tar(e) => Self::Tar(e),
            ItemError::Deku(e) => Self::Deku(e),
        }
    }
}
