//! Pacwoman library.
#![warn(
    missing_docs,
    clippy::missing_docs_in_private_items,
    clippy::empty_docs,
    clippy::missing_panics_doc
)]
#![feature(str_as_str)]
#![feature(impl_trait_in_bindings)]

use std::{
    ffi::OsString,
    io::{Read, Write},
    os::unix::fs::PermissionsExt,
    path::PathBuf,
};

use base64::Engine;
use bytes::Buf;
use gpgme::SignatureSummary;

/// A descriptor of a repository.
#[derive(Clone, Default, Debug)]
pub struct RepoDescriptor {
    /// The repo(i.e. core, extra, multilib)
    repo: String,
    /// The architecture to use.
    arch: String,
    /// The hash of the (repo).db file.
    hash: Option<String>,
}

impl RepoDescriptor {
    /// Creates a new [RepoDescriptor].
    pub fn new() -> Self {
        Self::default()
    }

    /// The repo(i.e. core, extra, multilib)
    pub fn repo(&self) -> String {
        self.repo.clone()
    }
    /// Sets the repo.
    pub fn set_repo(&mut self, repo: String) -> &mut Self {
        self.repo = repo;
        self
    }

    /// The architecture to use.
    pub fn arch(&self) -> String {
        self.arch.clone()
    }
    /// Sets [`arch`].
    /// [`arch`]: [RepoDescriptor::arch]
    pub fn set_arch(&mut self, arch: String) -> &mut Self {
        self.arch = arch;
        self
    }

    /// The hash of the (repo).db file.
    pub fn hash(&self) -> Option<String> {
        self.hash.clone()
    }
    /// Sets [`hash`].
    /// [`hash`]: [RepoDescriptor::hash]
    pub fn set_hash(&mut self, hash: Option<String>) -> &mut Self {
        self.hash = hash;
        self
    }

    /// Formats the repo descriptor into a string.
    #[allow(clippy::missing_panics_doc)] // function won't ever actually panic
    pub fn format(&self) -> String {
        if self.hash().is_some() {
            format!("{}-{}-{}", self.hash().unwrap(), self.repo(), self.arch())
        } else {
            format!("{}-{}", self.repo(), self.arch())
        }
    }
}

/// The identifier for a package. Used to locate the package.
#[derive(Clone)]
pub struct Package {
    /// The repo(i.e. core, extra, multilib)
    repo: RepoDescriptor,
    /// The filename of the package.
    filename: String,
}

impl Default for Package {
    fn default() -> Self {
        Self::new()
    }
}

impl Package {
    /// Creates a new [Package].
    pub fn new() -> Self {
        Self {
            repo: RepoDescriptor::new(),
            filename: String::new(),
        }
    }
    /// The repo(i.e. core, extra, multilib)
    pub fn repo(&self) -> RepoDescriptor {
        self.repo.clone()
    }
    /// Sets the repo.
    pub fn set_repo(&mut self, repo: RepoDescriptor) -> &mut Self {
        self.repo = repo;
        self
    }

    /// The filename of the package.
    pub fn filename(&self) -> String {
        self.filename.clone()
    }
    /// Sets [`filename`].
    /// [`filename`]: [Package::filename]
    pub fn set_filename(&mut self, filename: String) -> &mut Self {
        self.filename = filename;
        self
    }
}

/// A mirror to use.
#[derive(Clone, Debug)]
pub struct Mirror(url::Url);

impl Mirror {
    /// Creates a new [Mirror].
    pub fn new(base_url: url::Url) -> Self {
        Self(base_url)
    }
    /// Returns the base URL of the mirror and, similar to pacman, replacements for the repo and arch.
    /// (i.e. https://geo.mirror.pkgbuild.com/$repo/os/$arch).
    pub fn base(&self) -> url::Url {
        self.0.clone()
    }
    /// Sets the base URL of the mirror.
    pub fn set_base(&mut self, url: url::Url) -> &mut Self {
        self.0 = url;
        self
    }
    /// Substitutes a repo and architecture into the base url.
    #[allow(clippy::missing_panics_doc)] // never will panic
    pub fn substitute(&self, repo: &str, arch: &str, path: &str) -> url::Url {
        url::Url::parse(
            &(self
                .base()
                .as_str()
                .replace("$repo", repo)
                .replace("$arch", arch)
                + "/"
                + path),
        )
        .unwrap()
    }
    /// Makes an HTTP request to see if the server is reachable. Specifically, it attempts to reach
    /// the core.db file in the core repository.
    #[allow(clippy::missing_panics_doc)] // never will panic
    pub fn is_reachable(&self, arch: &str) -> bool {
        let url = self.substitute("core", arch, "core.db");
        if let Ok(res) = reqwest::blocking::get(url) {
            res.status().is_success()
        } else {
            false
        }
    }
}

/// Get the current user. It will first try the effective username. If the effective user has been
/// deleted, then it will try the current user. If the current user has also been deleted, then
/// it will as a last resort create a unique string in the format `deleted_user_e{e}_c{c}`
/// where {e} is the effective user uid and {c} is the current user uid.
pub fn get_current_user() -> OsString {
    let username = users::get_effective_username();
    if let Some(user) = username {
        user
    } else {
        let username = users::get_current_username();
        if let Some(user) = username {
            user
        } else {
            format!(
                "deleted_user_e{}_c{}",
                users::get_effective_uid(),
                users::get_current_uid()
            )
            .into()
        }
    }
}

/// Returns the base dir for pacwoman. Use this instead of hard-coding /pacwoman for the case
/// of debugging in a different directory.
///
/// This directory is not guaranteed to exist! If you need it to, use [base_dir]!
pub fn get_base_dir() -> PathBuf {
    PathBuf::from("/pacwoman")
}

/// Same as [get_base_dir]; however, this function will create the directory if it doesn't
/// exist. If [std::fs::create_dir_all] returns an error, it will be propagated.
pub fn base_dir() -> std::io::Result<PathBuf> {
    let dir = get_base_dir();
    std::fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// Get the config directory. If for the entire system, then it will be /pacwoman/config, otherwise
/// it will be /pacwoman/user/{user name from [get_current_user]}/config.
///
/// This directory is not guaranteed to exist! If you need it to, use [config_directory]!
pub fn get_config_directory(system: bool) -> PathBuf {
    if system {
        get_base_dir().join("config")
    } else {
        get_base_dir().join(format!(
            "user/{}/config",
            get_current_user().to_string_lossy()
        ))
    }
}

/// Same as [get_config_directory]; however, this function will create the directory if it doesn't
/// exist. If [std::fs::create_dir_all] returns an error, it will be propagated.
pub fn config_directory(system: bool) -> std::io::Result<PathBuf> {
    let dir = get_config_directory(system);
    std::fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// Get the store directory. If for the entire system, then it will be /pacwoman/store, otherwise
/// it will be /pacwoman/user/{user name from [get_current_user]}/store.
///
/// This directory is not guaranteed to exist! If you need it to, use [store_directory]!
pub fn get_store_directory(system: bool) -> PathBuf {
    if system {
        get_base_dir().join("store")
    } else {
        get_base_dir().join(format!(
            "user/{}/store",
            get_current_user().to_string_lossy()
        ))
    }
}

/// Same as [get_store_directory]; however, this function will create the directory if it doesn't
/// exist. If [std::fs::create_dir_all] returns an error, it will be propagated.
pub fn store_directory(system: bool) -> std::io::Result<PathBuf> {
    let dir = get_store_directory(system);
    std::fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// Gets the store directory for a repo.
///
/// This directory is not guaranteed to exist! See [repo_store_directory] if you need it too.
pub fn get_repo_store_directory(system: bool, repo: RepoDescriptor) -> PathBuf {
    get_store_directory(system).join(repo.format())
}

/// Same as [get_repo_store_directory] but creates the directory(s) if they don't exist.
pub fn repo_store_directory(system: bool, repo: RepoDescriptor) -> std::io::Result<PathBuf> {
    let dir = get_repo_store_directory(system, repo);
    std::fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// Get the index directory. This is always a system-wide directory of /pacwoman/index.
/// This directory is subject to change; because of this, use this method instead of
/// hard-coding the directory.
///
/// This directory is not guaranteed to exist! If you need it to, use [index_directory]!
pub fn get_index_directory() -> PathBuf {
    get_base_dir().join("index")
}

/// Same as [get_index_directory]; however, this function will create the directory if it doesn't
/// exist. If [std::fs::create_dir_all] returns an error, it will be propagated.
pub fn index_directory() -> std::io::Result<PathBuf> {
    let dir = get_index_directory();
    std::fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// Returns the index directory of a repository. This does not include the hash; this should be
/// assumed to symlink to the correct, hashed directory.
///
/// This directory is not guaranteed to exist! See [repo_index_dir] if you need it too.
pub fn get_repo_index_dir(repo: RepoDescriptor) -> PathBuf {
    get_index_directory().join(repo.format())
}

/// Same as [get_repo_index_dir], except it will create the directory if it doesn't exist.
/// If [std::fs::create_dir_all] returns an error, it will be propagated.
pub fn repo_index_dir(repo: RepoDescriptor) -> std::io::Result<PathBuf> {
    let dir = get_repo_index_dir(repo);
    std::fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// Directory for storing GPG keys and other miscellaneous GPG stuff.
///
/// This directory is not guaranteed to exist! See [gpg_dir] if you need it too.
pub fn get_gpg_dir() -> PathBuf {
    get_base_dir().join("keys")
}

/// Same as [get_gpg_dir], except it will create the directory if it doesn't exist.
/// If [std::fs::create_dir_all] returns an error, it will be propagated.
pub fn gpg_dir() -> std::io::Result<PathBuf> {
    let dir = get_gpg_dir();
    std::fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// Directory for storing binaries. Equivalent to the root directory. Contains directories such
/// as etc, usr, var, and others.
///
/// This directory is not guaranteed to exist! See [bin_dir] if you need it too.
pub fn get_bin_dir(system: bool) -> PathBuf {
    if system {
        get_base_dir().join("bin")
    } else {
        get_base_dir().join(format!("user/{}/bin", get_current_user().to_string_lossy()))
    }
}

/// Same as [get_bin_dir], except it will create the directory if it doesn't exist.
/// If [std::fs::create_dir_all] returns an error, it will be propagated.
pub fn bin_dir(system: bool) -> std::io::Result<PathBuf> {
    let dir = get_bin_dir(system);
    std::fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// Populates the index with information for a certain repo from a certain mirror. Will
/// overwrite any symlink for the repo.
///
/// # Panics
/// Should never panic as .unwrap() is only used on things that should always be
/// Some/Ok.
pub fn populate_index(mirror: Mirror, repo: RepoDescriptor) -> std::io::Result<PathBuf> {
    if !mirror.is_reachable(&repo.arch) {
        return Err(std::io::Error::new(
            std::io::ErrorKind::HostUnreachable,
            "mirror is unreachable",
        ));
    }
    let url = mirror.substitute(&repo.repo(), &repo.arch(), &format!("{}.db", repo.repo()));

    if let Ok(res) = reqwest::blocking::get(url) {
        let bytes = res.bytes().unwrap().to_vec();
        let bytes_reader_bytes = bytes.clone();
        let bytes_reader = bytes_reader_bytes.reader();
        let tar = flate2::read::GzDecoder::new(bytes_reader);
        let mut archive = tar::Archive::new(tar);

        let digest = sha256::digest(bytes);

        let index_dir =
            index_directory()?.join(format!("{}-{}-{}", digest, repo.repo(), repo.arch()));

        if std::fs::exists(&index_dir)? {
            let _ = std::fs::remove_dir_all(get_repo_index_dir(repo.clone()));

            std::os::unix::fs::symlink(index_dir, get_repo_index_dir(repo.clone()))?;

            return repo_index_dir(repo);
        }

        std::fs::create_dir_all(&index_dir)?;

        archive.unpack(&index_dir)?;

        let mut package_list: Vec<OsString> = vec![];

        for item in std::fs::read_dir(&index_dir)? {
            if item.is_err() {
                continue;
            }
            let item = item.unwrap();
            if item.file_type().is_err() {
                continue;
            }
            if item.file_type().unwrap().is_dir() {
                package_list.push(item.file_name());
            }
        }

        let mut packages = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .truncate(false)
            .open(index_directory()?.join("PACKAGES"))?;

        packages.write_fmt(format_args!(
            "{}-{}-{}: {}\n",
            digest,
            repo.repo(),
            repo.arch(),
            package_list.join(&OsString::from(" ")).to_string_lossy()
        ))?;
        drop(packages);

        std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(index_dir.join("SOURCE"))?
            .write_fmt(format_args!(
                "{}\n{}\n{}",
                mirror.substitute(&repo.repo(), &repo.arch(), ""),
                time::OffsetDateTime::now_utc()
                    .format(&time::format_description::well_known::Iso8601::DATE_TIME_OFFSET)
                    .unwrap(),
                digest
            ))?;

        if std::fs::exists(index_directory()?.join("REPOS"))? {
            let mut repos = std::fs::OpenOptions::new()
                .read(true)
                .open(index_directory()?.join("REPOS"))?;

            let mut repos_data = String::new();
            let mut repos_data_out = String::new();

            repos.read_to_string(&mut repos_data)?;

            drop(repos);

            let mut need_append = true;
            for line in repos_data.split("\n") {
                if line.starts_with(&format!("{}-{}: ", repo.repo(), repo.arch())) {
                    need_append = false;
                    repos_data_out += &format!("{} {}\n", line, digest);
                } else {
                    repos_data_out += &(line.to_owned() + "\n");
                }
            }
            if need_append {
                repos_data_out += &format!("{}-{}: {}\n", repo.repo(), repo.arch(), digest);
            }

            let mut repos = std::fs::OpenOptions::new()
                .create(true)
                .write(true)
                .truncate(true)
                .open(index_directory()?.join("REPOS"))?;
            repos.write_all(repos_data_out.as_bytes())?;
            drop(repos);
        } else {
            std::fs::write(
                index_directory()?.join("REPOS"),
                format!("{}-{}: {}\n", repo.repo(), repo.arch(), digest),
            )?;
        }

        let _ = std::fs::remove_dir_all(get_repo_index_dir(repo.clone()));

        std::os::unix::fs::symlink(index_dir, get_repo_index_dir(repo.clone()))?;

        Ok(repo_index_dir(repo)?)
    } else {
        Err(std::io::Error::new(
            std::io::ErrorKind::HostUnreachable,
            "mirror does not have repo",
        ))
    }
}

/// Get a list of hashes provided by the repo.
#[allow(clippy::missing_panics_doc)] // Shouldn't ever panic, but could be wrong.
pub fn read_repos(repo: RepoDescriptor) -> std::io::Result<Vec<(PathBuf, String)>> {
    let mut repos = std::fs::OpenOptions::new()
        .read(true)
        .open(index_directory()?.join("REPOS"))?;

    let mut repos_data = String::new();

    repos.read_to_string(&mut repos_data)?;

    drop(repos);

    let repos = repos_data;
    for line in repos.split("\n") {
        if line.starts_with(&format!("{}-{}: ", repo.repo(), repo.arch())) {
            let mut strings: Vec<String> = vec![];
            for hash in line.split_once(": ").unwrap().1.split(" ") {
                strings.push(hash.to_owned() + &repo.format());
            }
            let mut out: Vec<(PathBuf, String)> = vec![];
            for ele in strings {
                out.push((index_directory()?.join(PathBuf::from(&ele)), ele));
            }
            return Ok(out);
        }
    }
    Err(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        "repo not found in REPOS file",
    ))
}

/// Get a list of (Package name and version, repo, path to index directory with the package)
/// for a certain package name. This function uses [read_desc] internally after performing
/// preliminary checks to make sure that only packages that have the exact name of
/// package_name will be returned. If this was not used, then this would create false
/// positives of any package that is in the format `(package_name)-.*` in a psuedo-regex.
#[allow(clippy::missing_panics_doc)] // will never panic
pub fn locate_package(
    package_name: String,
) -> std::io::Result<Vec<(String, RepoDescriptor, PathBuf)>> {
    let mut packages = std::fs::OpenOptions::new()
        .read(true)
        .open(index_directory()?.join("PACKAGES"))?;

    let mut packages_data = String::new();

    packages.read_to_string(&mut packages_data)?;

    drop(packages);

    let packages = packages_data.split("\n");

    let mut out: Vec<(String, RepoDescriptor, PathBuf)> = vec![];

    for line in packages {
        if line.is_empty() {
            continue;
        }
        let segments = line.split_once(": ").unwrap();
        let prefix = segments.0;
        let suffix = segments.1;
        if suffix.contains(&(" ".to_string() + &package_name + "-")) {
            let arch = prefix.split("-").last().unwrap().to_string();
            let repo = prefix
                .strip_suffix(&("-".to_string() + &arch))
                .unwrap()
                .split_once("-")
                .unwrap()
                .1
                .to_string();

            let descriptor = RepoDescriptor::new()
                .set_arch(arch.clone())
                .set_repo(repo.clone())
                .clone();

            let mut package = String::new();
            let start = suffix
                .find(&(" ".to_string() + &package_name + "-"))
                .unwrap()
                + 1; // add one because otherwise that would be the space

            for char in suffix[start..].chars() {
                if char == ' ' {
                    break;
                }
                package += &char.to_string();
            }

            let desc = read_desc(package.clone(), descriptor.clone());
            if desc.is_err() {
                continue;
            }

            if desc?.name != package_name {
                continue;
            }

            out.push((
                package.clone(),
                descriptor,
                index_directory()?
                    .join(format!("{}-{}", repo, arch))
                    .join(package),
            ));
        }
    }

    if !out.is_empty() {
        return Ok(out);
    }

    Err(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        "cannot find package",
    ))
}

/// Information provided by a packages desc file.
#[derive(Clone, Default, Debug)]
pub struct PackageDesc {
    /// The filename of the package in the repo.
    pub filename: String,
    /// The name of the package.
    pub name: String,
    /// Base name for split packages. Unknown how common?
    pub base: Option<String>,
    /// The version of the package. Generally semver, unknown if must be?
    pub version: String,
    /// Description of the package.
    pub description: Option<String>,
    /// Download size.
    pub csize: Option<usize>,
    /// Installed size.
    pub isize: Option<usize>,
    /// MD5 checksum of the package. Recommended not to use and to instead use [`sha256sum`].
    /// Stored as hex in the desc file.
    /// [`sha256sum`]: [PackageDesc::sha256sum]
    pub md5sum: String,
    /// SHA256 checksum of the package.
    /// Stored as hex in the desc file.
    pub sha256sum: String,
    /// PGP signature of the package.
    /// Stored as base64 in the desc file.
    pub pgpsig: Vec<u8>,
    /// URL to a website for the package.
    pub url: Option<String>,
    /// An array of licenses for the package.
    pub license: Vec<String>,
    /// The architecture of the package. Not always the same as the repos; if not, generally
    /// something like `any`.
    pub arch: Option<String>,
    /// The build date of the package as a unix timestamp.
    pub builddate: Option<u64>,
    /// The packager of the package. Generally in the format `(name) <(email)>` (may be enforced?
    /// unclear).
    pub packager: Option<String>,
    /// Any packages the package depends on.
    pub depends: Vec<String>,
    /// Packages required for the package's testing suite. Unsure if tests can be performed even?
    pub checkdepends: Vec<String>,
    /// Packages optionally required. The first field is the package name, and the second is
    /// the human-readable reason.
    pub optdepends: Vec<(String, String)>,
    /// Packages required to build the package. May be useless.
    pub makedepends: Vec<String>,
}

/// Reads the desc file of the package into a [PackageDesc]. Will return an error if required
/// fields don't exist. If they have an empty value, it will work. An error will also be
/// returned if any calls such as opening a file fail.
#[allow(clippy::missing_panics_doc)] // as far as i know shouldn't ever panic
pub fn read_desc(package: String, repo: RepoDescriptor) -> std::io::Result<PackageDesc> {
    let path = index_directory()?
        .join(repo.format())
        .join(package)
        .join("desc");

    let mut desc = std::fs::OpenOptions::new().read(true).open(path)?;

    let mut desc_data = String::new();

    desc.read_to_string(&mut desc_data)?;

    drop(desc);

    let desc_data = desc_data.split("\n").collect::<Vec<&str>>();

    let mut out = PackageDesc::default();

    if let Some(loc) = desc_data.iter().position(|item| (*item) == "%FILENAME%") {
        let loc = loc + 1; // value is on next line
        out.filename = desc_data[loc].to_string();
        if let Some(loc) = desc_data.iter().position(|item| (*item) == "%NAME%") {
            let loc = loc + 1; // value is on next line
            out.name = desc_data[loc].to_string();
            if let Some(loc) = desc_data.iter().position(|item| (*item) == "%VERSION%") {
                let loc = loc + 1; // value is on next line
                out.version = desc_data[loc].to_string();

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%BASE%") {
                    let loc = loc + 1; // value is on next line
                    out.base = Some(desc_data[loc].to_string());
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%DESC%") {
                    let loc = loc + 1; // value is on next line
                    out.description = Some(desc_data[loc].to_string());
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%CSIZE%") {
                    let loc = loc + 1; // value is on next line
                    let val = desc_data[loc].parse();
                    if val.is_err() {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidData,
                            "csize is invalid value",
                        ));
                    }
                    out.csize = Some(val.unwrap());
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%ISIZE%") {
                    let loc = loc + 1; // value is on next line
                    let val = desc_data[loc].parse();
                    if val.is_err() {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidData,
                            "isize is invalid value",
                        ));
                    }
                    out.isize = Some(val.unwrap());
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%MD5SUM%") {
                    let loc = loc + 1; // value is on next line
                    out.md5sum = desc_data[loc].to_string();
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%SHA256SUM%") {
                    let loc = loc + 1; // value is on next line
                    out.sha256sum = desc_data[loc].to_string();
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%PGPSIG%") {
                    let loc = loc + 1; // value is on next line
                    let val =
                        base64::engine::general_purpose::STANDARD.decode(desc_data[loc].as_bytes());
                    if val.is_err() {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidData,
                            "pgpsig is invalid value",
                        ));
                    }
                    out.pgpsig = val.unwrap();
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%URL%") {
                    let loc = loc + 1; // value is on next line
                    out.url = Some(desc_data[loc].to_string());
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%LICENSE%") {
                    let loc = loc + 1; // value is on next line
                    let mut val: Vec<String> = vec![];

                    for line in &desc_data[loc..] {
                        let line = *line;
                        if line.is_empty() {
                            break;
                        }
                        val.push(line.to_string());
                    }

                    out.license = val;
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%ARCH%") {
                    let loc = loc + 1; // value is on next line
                    out.arch = Some(desc_data[loc].to_string());
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%BUILDDATE%") {
                    let loc = loc + 1; // value is on next line
                    let val = desc_data[loc].parse();
                    if val.is_err() {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidData,
                            "builddate is invalid value",
                        ));
                    }
                    out.builddate = Some(val.unwrap());
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%PACKAGER%") {
                    let loc = loc + 1; // value is on next line
                    out.packager = Some(desc_data[loc].to_string());
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%DEPENDS%") {
                    let loc = loc + 1; // value is on next line
                    let mut val: Vec<String> = vec![];

                    for line in &desc_data[loc..] {
                        let line = *line;
                        if line.is_empty() {
                            break;
                        }
                        val.push(line.to_string());
                    }

                    out.depends = val;
                }

                if let Some(loc) = desc_data
                    .iter()
                    .position(|item| (*item) == "%CHECKDEPENDS%")
                {
                    let loc = loc + 1; // value is on next line
                    let mut val: Vec<String> = vec![];

                    for line in &desc_data[loc..] {
                        let line = *line;
                        if line.is_empty() {
                            break;
                        }
                        val.push(line.to_string());
                    }

                    out.checkdepends = val;
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%MAKEDEPENDS%") {
                    let loc = loc + 1; // value is on next line
                    let mut val: Vec<String> = vec![];

                    for line in &desc_data[loc..] {
                        let line = *line;
                        if line.is_empty() {
                            break;
                        }
                        val.push(line.to_string());
                    }

                    out.makedepends = val;
                }

                if let Some(loc) = desc_data.iter().position(|item| (*item) == "%OPTDEPENDS%") {
                    let loc = loc + 1; // value is on next line
                    let mut val: Vec<(String, String)> = vec![];

                    for line in &desc_data[loc..] {
                        let line = line.to_string();
                        if line.is_empty() {
                            break;
                        }
                        let l = line.split_once(": ").unwrap();
                        let l = (l.0.to_string(), l.1.to_string());
                        val.push(l);
                    }

                    out.optdepends = val;
                }

                Ok(out)
            } else {
                Err(std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    "cannot find version in desc file of package",
                ))
            }
        } else {
            Err(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "cannot find name in desc file of package",
            ))
        }
    } else {
        Err(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "cannot find filename in desc file of package",
        ))
    }
}

/// Gets the [gpgme] context and sets the appropriate properties.
pub fn get_gpg_ctx() -> std::io::Result<gpgme::Context> {
    let mut gpg_ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?;
    gpg_ctx.set_engine_home_dir(gpg_dir()?.to_string_lossy().into_owned())?;
    Ok(gpg_ctx)
}

/// The PGP public key file for arch linux signing keys.
const ARCHLINUX_PGP_PUBLIC_KEY: &[u8] = include_bytes!("archlinux.gpg");

/// Initalizes the GPG keyring and other GPG stuff.
pub fn init_gpg() -> std::io::Result<()> {
    let mut gpg_ctx = get_gpg_ctx()?;

    print!("Generating master key...");
    gpg_ctx.generate_key::<Vec<u8>, Vec<u8>>(
        r#"<GnupgKeyParms format="internal">
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign
Name-Real: Pacwoman Keyring Master Key
Name-Email: pacwoman@localhost
Expire-Date: 0
%no-protection
</GnupgKeyParms>"#,
        None,
        None,
    )?;
    println!(" Done.");

    gpg_ctx.import(ARCHLINUX_PGP_PUBLIC_KEY)?;

    Ok(())
}

/// Downloads, verifies, and unpacks a package from the provided mirror. The package value
/// provided should be the file name provided in a [PackageDesc].
///
/// # Panics
/// Panics if there are any errors with downloading because right now I don't feel like
/// implementing it to fail gently.
pub fn receive_package(
    mirror: Mirror,
    repo: RepoDescriptor,
    package: String,
    system: bool,
) -> std::io::Result<PathBuf> {
    let path = index_directory()?
        .join(repo.format())
        .join(&package)
        .join("desc");

    let mut desc = std::fs::OpenOptions::new().read(true).open(path)?;

    let mut desc_data = String::new();

    desc.read_to_string(&mut desc_data)?;

    drop(desc);

    let desc_hash = sha256::digest(desc_data);

    let desc = read_desc(package.clone(), repo.clone())?;

    let sig_url = mirror.substitute(
        &repo.clone().repo(),
        &repo.arch(),
        &(desc.filename.clone() + ".sig"),
    );

    let url = mirror.substitute(&repo.repo(), &repo.arch(), &desc.filename.clone());

    let dir = repo_store_directory(system, repo)?.join(format!("{}-{}", desc_hash, package));

    if std::fs::exists(index_directory()?.join("RECIEVED"))? {
        let mut packages = std::fs::OpenOptions::new()
            .read(true)
            .open(index_directory()?.join("RECIEVED"))?;

        let mut packages_data = String::new();

        packages.read_to_string(&mut packages_data)?;

        drop(packages);

        let packages: Vec<&str> = packages_data.split("\n").collect();

        if packages.contains(&format!("{}-{}", desc_hash, package).as_str()) {
            return Ok(dir);
        }
    }

    let sig_data = reqwest::blocking::get(sig_url)
        .unwrap()
        .bytes()
        .unwrap()
        .to_vec();

    let package_data = reqwest::blocking::get(url)
        .unwrap()
        .bytes()
        .unwrap()
        .to_vec();

    let mut gpg_ctx = get_gpg_ctx()?;
    let res = gpg_ctx
        .verify_detached(&sig_data, &package_data)?
        .signatures()
        .next()
        .unwrap()
        .summary();

    match res {
        SignatureSummary::VALID | SignatureSummary::GREEN => {}
        sum => {
            if !sum.is_empty() {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    format!("signature invalid, error {:#?}", sum),
                ));
            }
        }
    }

    let sum = sha256::digest(&package_data);

    if desc.sha256sum != sum {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "SHA256 checksum does not match provided sum in desc, try updating the index or try receiving the package again",
        ));
    }

    let mut reader = package_data.reader();

    let tar = ruzstd::decoding::StreamingDecoder::new(&mut reader).unwrap();
    let mut archive = tar::Archive::new(tar);
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir)?;
    archive.set_overwrite(true);
    archive.set_preserve_ownerships(false);
    archive.set_preserve_mtime(true);
    archive.unpack(&dir)?;

    let mut packages = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .truncate(false)
        .open(index_directory()?.join("RECIEVED"))?;

    packages.write_fmt(format_args!("{}-{}\n", desc_hash, package))?;
    drop(packages);

    Ok(dir)
}

/// Recieves a package and all of it's dependencies. Returns a list of paths to each package.
pub fn recieve_package_and_dependencies(
    mirror: Mirror,
    repo: RepoDescriptor,
    package: String,
    system: bool,
) -> std::io::Result<Vec<PathBuf>> {
    let mut out: Vec<PathBuf> = vec![];

    print!("[STAT] Downloading package {}...", package);
    out.push(receive_package(
        mirror.clone(),
        repo.clone(),
        package.clone(),
        system,
    )?);
    print!(" Done.\n[STAT] Parsing desc...");

    let path = index_directory()?
        .join(repo.clone().format())
        .join(&package)
        .join("desc");

    let mut desc = std::fs::OpenOptions::new().read(true).open(path)?;

    let mut desc_data = String::new();

    desc.read_to_string(&mut desc_data)?;

    drop(desc);

    let desc = read_desc(package.clone(), repo.clone())?;

    println!(" Done.");

    for package in desc.depends {
        print!("[STAT] Downloading package {}...", package);

        let package = package
            .split_once("=")
            .unwrap_or((&package, ""))
            .0
            .to_string();
        let package = package.strip_suffix(">").unwrap_or(&package).to_string();
        let package = package.strip_suffix(".so").unwrap_or(&package).to_string();

        out.push(receive_package(
            mirror.clone(),
            repo.clone(),
            locate_package(package)?[0].0.clone(),
            system,
        )?);
        println!(" Done.");
    }
    Ok(out)
}

/// Recieves a package and it's dependencies and installs it.
#[allow(clippy::missing_panics_doc)] // won't ever actually panic
pub fn install_package(
    mirror: Mirror,
    repo: RepoDescriptor,
    package: String,
    system: bool,
) -> std::io::Result<()> {
    let out = bin_dir(system)?;
    for path in recieve_package_and_dependencies(mirror, repo, package, system)? {
        if std::fs::exists(index_directory()?.join("INSTALLED"))? {
            let mut packages = std::fs::OpenOptions::new()
                .read(true)
                .open(index_directory()?.join("INSTALLED"))?;

            let mut packages_data = String::new();

            packages.read_to_string(&mut packages_data)?;

            drop(packages);

            let packages: Vec<&str> = packages_data.split("\n").collect();

            if packages.contains(&(path.file_name().unwrap().to_string_lossy().as_str())) {
                continue;
            }
        }

        /// Fuck me.
        #[derive(Clone)]
        struct VisitDir<'a> {
            /// Fuck me.
            f: &'a dyn Fn(std::io::Result<std::fs::DirEntry>, VisitDir) -> std::io::Result<()>,
        }

        let diritem = VisitDir {
            f: &|entry: std::io::Result<std::fs::DirEntry>,
                 visit_dir: VisitDir|
             -> std::io::Result<()> {
                let entry = entry?;

                let entry_path = PathBuf::from(
                    entry
                        .path()
                        .to_string_lossy()
                        .strip_prefix(path.to_string_lossy().as_str())
                        .unwrap_or(&entry.path().to_string_lossy())
                        .strip_prefix("/")
                        .unwrap_or(&entry.path().to_string_lossy()),
                );

                if entry.file_name() == *".BUILDINFO"
                    || entry.file_name() == *".MTREE"
                    || entry.file_name() == *".PKGINFO"
                {
                    return Ok(());
                }
                if entry.file_name() == *".INSTALL" {
                    std::fs::copy(path.join(&entry_path), out.join(&entry_path))?;
                    return Ok(());
                }

                if entry.file_type()?.is_dir() {
                    if !std::fs::exists(out.join(&entry_path))? {
                        std::fs::create_dir_all(out.join(&entry_path))?;
                    }
                    for entry in std::fs::read_dir(path.join(&entry_path))? {
                        (visit_dir.clone().f)(entry, visit_dir.clone())?;
                    }
                } else {
                    if !std::fs::exists(out.join(entry_path.parent().unwrap()))? {
                        std::fs::create_dir_all(out.join(entry_path.parent().unwrap()))?;
                    }
                    if std::fs::exists(out.join(&entry_path))? {
                        std::fs::remove_file(&entry_path)?;
                    }
                    std::os::unix::fs::symlink(path.join(&entry_path), out.join(&entry_path))?;
                }
                Ok(())
            },
        };

        for entry in std::fs::read_dir(&path)? {
            (diritem.clone().f)(entry, diritem.clone())?;
        }

        if std::fs::exists(out.join(".INSTALL"))? {
            if users::get_effective_uid() != 0 {
                println!(
                    "[WARN] .INSTALL script for package {} is being run without root.",
                    path.file_name().unwrap().to_string_lossy()
                );
                println!("[WARN] The script may give permission errors or ask for authentication.");
            }

            if !std::process::Command::new("/usr/bin/bash")
                .arg("-c")
                .arg("source ./.INSTALL && post_install && post_upgrade")
                .current_dir("/")
                .spawn()?
                .wait()?
                .success()
            {
                println!(
                    "[WARN] .INSTALL script for package {} failed!",
                    path.file_name().unwrap().to_string_lossy()
                );
            }

            std::fs::remove_file(out.join(".INSTALL"))?;
        }

        let mut packages = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .truncate(false)
            .open(index_directory()?.join("INSTALLED"))?;

        packages.write_fmt(format_args!(
            "{}\n",
            path.file_name().unwrap().to_string_lossy()
        ))?;
        drop(packages);
    }

    Ok(())
}

/// Remove a package from the store. This WILL lead to dangling symlinks because I don't
/// feel like fixing that!
pub fn remove_package(package: String, system: bool, repo: RepoDescriptor) -> std::io::Result<()> {
    let path = index_directory()?
        .join(repo.format())
        .join(&package)
        .join("desc");

    let mut desc = std::fs::OpenOptions::new().read(true).open(path)?;

    let mut desc_data = String::new();

    desc.read_to_string(&mut desc_data)?;

    drop(desc);

    let desc_hash = sha256::digest(desc_data);

    std::fs::remove_dir_all(store_directory(system)?.join(format!("{}-{}", desc_hash, package)))?;

    Ok(())
}

/// Creates all of the necessary directories, for the system if running as root, and
/// for the current user if not. If [std::fs::create_dir_all] returns an error, it will be
/// propagated.
pub fn create_directories(system: bool) -> Result<(), std::io::Error> {
    if system {
        store_directory(true)?;
        config_directory(true)?;
        index_directory()?;
        gpg_dir()?;
        bin_dir(true)?;
        let user_dir = base_dir()?.join("user");
        std::fs::create_dir_all(&user_dir)?;
        std::fs::set_permissions(user_dir, std::fs::Permissions::from_mode(0o777))?;
    } else {
        let user_dir = base_dir()?.join("user").join(get_current_user());
        std::fs::create_dir_all(&user_dir)?;
        std::fs::set_permissions(user_dir, std::fs::Permissions::from_mode(0o700))?;

        store_directory(false)?;
        config_directory(false)?;
        bin_dir(false)?;
    }

    Ok(())
}
