#![allow(dead_code)]
use std::process::Output;

pub use {error::Error, hooks::Hooks};
mod hooks;

use {
    crate::{
        tar::{Archive, Node},
        Entry, Group, Package, User,
    },
    rayon::prelude::{IntoParallelRefIterator, ParallelIterator},
    sha2::{Digest, Sha256},
    std::{
        ffi::OsStr,
        fmt::Write as _,
        fs::{self, DirBuilder, File},
        io::{self, BufReader, BufWriter, Write},
        os::{
            self,
            unix::{fs::DirBuilderExt, prelude::OpenOptionsExt},
        },
        path::{Path, PathBuf},
        sync::{mpsc::Sender, Mutex},
    },
    zstd::Decoder,
};

#[derive(Debug)]
/// Represents a symbolic link
pub struct Link {
    pub path: PathBuf,
    pub target: PathBuf,
}

#[derive(Debug)]
/// Messages sent from and `Installer` to the calling thread
pub enum InstallMessage {
    ArchiveRead,
    /// The number of files to be extracted from the archive, sent before any
    /// extraction takes place to facilitate building eg. progress bars
    ArchiveLen(usize),
    /// A file has been successfully installed
    FileInstalled(PathBuf),
    /// A directory has been successfully created
    DirectoryCreated(PathBuf),
    /// A `Link` has been successfully created
    LinkCreated(Link),
    /// A `User` has been successfully created
    UserCreated(User),
    /// A `Group` has been successfully created
    GroupCreated(Group),
    /// A shell has been added to '/etc/shells'
    ShellAdded(String),
    /// The output of the post install script
    PostInstall(Output),
    /// Update the mandoc db
    Man,
    /// Update the info db
    Info(String),
    /// Glib schemas updated
    GlibSchemas,
}

/// Installs a package into it's specified rootfs
pub struct Installer<T: io::Read> {
    /// The filesystem root under which to install this package.
    /// Normally this is "/".
    root: PathBuf,
    reader: T,
}

impl Installer<Decoder<'_, BufReader<File>>> {
    /// creates a new installer from an archive path
    pub fn new_for_file<P: AsRef<OsStr>>(root: P, archive: P) -> Result<Self, io::Error> {
        let root = Path::new(&root).to_path_buf();
        let fd = File::open(Path::new(&archive))?;
        let decoder = Decoder::new(fd)?;
        Ok(Self::new(root, decoder))
    }
}

impl<T: io::Read> Installer<T> {
    /// Creates a new installer from an open archive stream
    pub fn new<P: AsRef<OsStr>>(root: P, reader: T) -> Self {
        let root = Path::new(&root).to_path_buf();
        Self { root, reader }
    }

    pub fn install(
        self,
        hooks: &mut Hooks,
        sender: Sender<InstallMessage>,
    ) -> Result<Package, Error> {
        let reader = Decoder::new(self.reader)?;
        let mut archive = Archive::read(reader)?;
        sender.send(InstallMessage::ArchiveRead)?;
        let Some(pr_node) = archive.pop("package.ron") else {
            return Err(Error::MissingManifest);
        };
        let mut buf = vec![];
        pr_node.write(&mut buf)?;
        let package: Package = ron::de::from_bytes(&buf)?;
        if let Some(ref users) = package.users {
            users.iter().for_each(|u| hooks.push_user(u.clone()));
        }
        if let Some(ref groups) = package.groups {
            groups.iter().for_each(|g| hooks.push_group(g.clone()));
        }
        if let Some(ref shells) = package.shells {
            shells.iter().for_each(|s| hooks.push_shell(s.clone()));
        }
        let mut db_pkgdir = crate::get_dbdir(Some(self.root.clone()));
        db_pkgdir.push(&package.name);
        if !db_pkgdir.exists() {
            fs::create_dir_all(&db_pkgdir)?;
        }
        pop_appstream(&mut archive, &db_pkgdir)?;
        pop_pinstall(&mut archive, hooks, &package.name)?;
        let len = archive.nodes.len();
        sender.send(InstallMessage::ArchiveLen(len))?;
        let mut db_file = db_pkgdir;
        db_file.push("package.ron");
        if db_file.exists() {
            fs::remove_file(&db_file)?;
        }
        let fd = File::create(&db_file)?;
        let writer = BufWriter::new(fd);
        pr_node.write(writer)?;
        let root = &self.root;
        let hooks = Mutex::new(hooks);
        archive
            .nodes
            .par_iter()
            .try_for_each_with(sender, |sender, node| {
                let mut path = root.clone();
                let fpath = node.header.file_path()?;
                if let Some(s) = node.header.prefix() {
                    if s.contains("/share/man/") {
                        let mut h = hooks.lock().unwrap();
                        h.push_man();
                    } else if s.contains("/share/info") {
                        hooks.lock().unwrap().push_info(fpath.to_str().unwrap());
                    } else if s.contains("/share/glib-2.0/schemas") {
                        let mut h = hooks.lock().unwrap();
                        h.push_glib_schema();
                    }
                }
                // Match up a package entry with a tar node
                let entry = package
                    .plist
                    .entries
                    .iter()
                    .find(|&x| match x {
                        Entry::File {
                            path,
                            sha256sum: _,
                            mode: _,
                            size: _,
                        } => path == &fpath,
                        Entry::Link { path, target: _ } | Entry::Directory { path, mode: _ } => {
                            path == &fpath
                        }
                    })
                    .unwrap();
                path = path.join(&fpath);
                if let Some(parent) = path.parent() {
                    if !parent.exists() {
                        fs::create_dir_all(parent)?;
                    }
                }
                if path.exists() {
                    if path.is_dir() {
                        fs::remove_dir(&path)?;
                    } else if path.is_symlink() || path.is_file() {
                        fs::remove_file(&path)?;
                    }
                }
                let msg = extract_entry(entry, node, path)?;
                sender.send(msg)?;
                Ok::<(), Error>(())
            })?;
        Ok(package)
    }
}

fn extract_entry(entry: &Entry, node: &Node, path: PathBuf) -> Result<InstallMessage, Error> {
    match entry {
        Entry::Directory { path: _, mode } => {
            DirBuilder::new().mode(*mode).create(&path)?;
            Ok(InstallMessage::DirectoryCreated(path))
        }
        Entry::Link { path: _, target } => {
            os::unix::fs::symlink(&path, target)?;
            Ok(InstallMessage::LinkCreated(Link {
                path,
                target: target.clone(),
            }))
        }
        Entry::File {
            path: _,
            sha256sum,
            mode,
            size: _,
        } => {
            let mut buf = vec![];
            node.write(&mut buf)?;
            let mut sum = String::new();
            let mut hasher = Sha256::new();
            hasher.update(&buf);
            let res = hasher.finalize();
            for c in res {
                write!(sum, "{c:02x}")?;
            }
            if sha256sum != &sum {
                return Err(Error::ChecksumMismatch);
            }
            let fd = File::options().mode(*mode).write(true).open(&path)?;
            let mut writer = BufWriter::new(fd);
            writer.write_all(&buf)?;
            Ok(InstallMessage::FileInstalled(path))
        }
    }
}

fn pop_appstream(archive: &mut Archive, db_pkgdir: &Path) -> Result<(), Error> {
    let appstream = archive.pop("appdata.xml");
    if let Some(node) = appstream {
        let mut appdatafile = db_pkgdir.to_path_buf();
        appdatafile.push("appdata.xml");
        let fd = File::open(appdatafile)?;
        let writer = BufWriter::new(fd);
        node.write(writer)?;
    }
    Ok(())
}

fn pop_pinstall(archive: &mut Archive, hooks: &mut Hooks, pkgname: &str) -> Result<(), Error> {
    let pinstall = archive.pop("postinstall.sh");
    if let Some(node) = pinstall {
        let mut path: PathBuf = ["/", "tmp", "hpk", pkgname].iter().collect();
        if !path.exists() {
            fs::create_dir_all(&path)?;
        }
        path.push("pinstall.sh");
        let fd = File::open(&path)?;
        let writer = BufWriter::new(fd);
        node.write(writer)?;
        hooks.push_pinstall(path);
    }
    Ok(())
}

mod error {
    use super::InstallMessage;
    use crate::{tar, Hooks};
    use ron::error::SpannedError;
    use std::{
        fmt, io,
        string::FromUtf8Error,
        sync::{
            mpsc::{SendError, Sender},
            PoisonError,
        },
    };

    #[derive(Debug)]
    pub enum Error {
        Fmt(fmt::Error),
        IO(io::Error),
        RonError(SpannedError),
        SendError(SendError<InstallMessage>),
        Tar(tar::Error),
        BadPath,
        ChecksumMismatch,
        MissingManifest,
        MutexError,
        Utf8,
    }

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

    impl std::error::Error for Error {
        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
            match self {
                Self::Fmt(e) => Some(e),
                Self::IO(e) => Some(e),
                Self::Tar(e) => Some(e),
                Self::SendError(e) => Some(e),
                Self::RonError(e) => Some(e),
                _ => None,
            }
        }
    }

    impl From<fmt::Error> for Error {
        fn from(value: fmt::Error) -> Self {
            Self::Fmt(value)
        }
    }

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

    impl From<PoisonError<Sender<InstallMessage>>> for Error {
        fn from(_value: PoisonError<Sender<InstallMessage>>) -> Self {
            Self::MutexError
        }
    }

    impl From<PoisonError<Vec<Hooks>>> for Error {
        fn from(_value: PoisonError<Vec<Hooks>>) -> Self {
            Self::MutexError
        }
    }

    impl From<SendError<InstallMessage>> for Error {
        fn from(value: SendError<InstallMessage>) -> Self {
            Self::SendError(value)
        }
    }

    impl From<SpannedError> for Error {
        fn from(value: SpannedError) -> Self {
            Self::RonError(value)
        }
    }

    impl From<FromUtf8Error> for Error {
        fn from(_value: FromUtf8Error) -> Self {
            Self::Utf8
        }
    }

    impl From<crate::tar::Error> for Error {
        fn from(value: crate::tar::Error) -> Self {
            Self::Tar(value)
        }
    }
}
