use {
    crate::{Arch, Package, Repository, Version},
    rayon::prelude::*,
    ron::{self, ser::PrettyConfig},
    serde::{Deserialize, Serialize},
    std::{
        collections::HashMap,
        error::Error,
        fmt,
        fs::{self, File},
        path::PathBuf,
        sync::Mutex,
    },
    url::Url,
    zstd::{Decoder, Encoder},
};

#[derive(Clone, Debug)]
/// Represents an updated package to be installed on the system
pub struct Update {
    pub name: String,
    pub version: Version,
    pub arch: Arch,
    pub release: u8,
    pub url: Url,
}

impl fmt::Display for Update {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}-{}_{}_{}",
            self.name, self.version, self.arch, self.release
        )
    }
}

impl Update {
    pub fn full_url(&self) -> Result<Url, url::ParseError> {
        self.url.join(&format!("{self}.tar.zstd"))
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
/// A struct representing all locally installed packages and all packages which
/// are available in a remote repository
pub struct Database {
    /// All locally installed packages
    pub packages: HashMap<String, Package>,
    /// All currently configured repositories
    pub available: HashMap<String, Repository>,
}

impl Database {
    pub fn update(&mut self) -> Result<(), Box<dyn Error>> {
        unimplemented!();
    }

    pub fn add_package(&mut self, package: Package) {
        let name = package.name.clone();
        self.packages.insert(name, package);
    }

    pub fn remove_package(&mut self, name: &str) -> Option<Package> {
        self.packages.remove(name)
    }

    pub fn get_upgradable(&mut self) -> Result<HashMap<String, Update>, Box<dyn Error>> {
        self.update()?;
        let updates = Mutex::new(HashMap::<String, Update>::new());
        self.packages.par_iter().for_each(|(key, local_package)| {
            for repo in self.available.values() {
                // Check if the remote has a package by this name
                if let Some(remote_package) = repo.packages.get(key) {
                    // Check if the remote package is an update
                    if remote_package.is_upgrade(local_package) {
                        if let Ok(mut updates) = updates.lock() {
                            // Check if we've already pulled in an update from another repo,
                            // and if so compare versions
                            if let Some(other_update) = updates.get(key) {
                                if remote_package.version > other_update.version
                                    || (remote_package.version == other_update.version
                                        && remote_package.release > other_update.release)
                                {
                                    // The remote version is an update to the already
                                    // pulled in update, so swap it out
                                    let update = Update {
                                        name: key.to_string(),
                                        version: remote_package.version.clone(),
                                        arch: remote_package.arch,
                                        release: remote_package.release,
                                        url: repo.base_url.clone(),
                                    };
                                    updates.insert(key.to_string(), update);
                                }
                            } else {
                                // First time we've seen this update, so insert it
                                // into our hashmap
                                let update = Update {
                                    name: key.to_string(),
                                    arch: remote_package.arch,
                                    version: remote_package.version.clone(),
                                    release: remote_package.release,
                                    url: repo.base_url.clone(),
                                };
                                updates.insert(key.to_string(), update);
                            }
                        }
                    }
                }
            }
        });
        let updates = updates.into_inner()?;
        Ok(updates)
    }

    pub fn from_file(prefix: Option<PathBuf>) -> Result<Self, Box<dyn Error>> {
        let db = crate::get_db(prefix);
        if let Some(dir) = db.parent() {
            if !dir.exists() {
                fs::create_dir_all(dir)?;
            }
        }
        let fd = File::open(db)?;
        let reader = Decoder::new(fd)?;
        let res = ron::de::from_reader(reader)?;
        Ok(res)
    }

    pub fn save_to_file(&self, prefix: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
        let db = crate::get_db(prefix);
        if let Some(dir) = db.parent() {
            if !dir.exists() {
                fs::create_dir_all(dir)?;
            }
        }
        let fd = File::create(db)?;
        let mut writer = Encoder::new(fd, 0)?;
        let cfg = PrettyConfig::new().struct_names(true);
        ron::ser::to_writer_pretty(&mut writer, self, cfg)?;
        writer.finish()?;
        Ok(())
    }

    pub fn rebuild(_prefix: Option<PathBuf>) -> Result<Self, Box<dyn Error>> {
        unimplemented!();
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn update_to_string() {
        let up = Update {
            name: "hpk".to_string(),
            version: Version::Number(42),
            arch: Arch::aarch64,
            release: 1,
            url: Url::parse("https://hitchhiker-linux.org/pub/packages/").unwrap(),
        }
        .to_string();
        assert_eq!("hpk-42_aarch64_1".to_string(), up);
    }

    #[test]
    fn upate_full_url() {
        let up = Update {
            name: "hpk".to_string(),
            version: Version::Number(42),
            arch: Arch::aarch64,
            release: 1,
            url: Url::parse("https://hitchhiker-linux.org/pub/packages/").unwrap(),
        };
        let full_url = up.full_url().unwrap().to_string();
        assert_eq!(
            "https://hitchhiker-linux.org/pub/packages/hpk-42_aarch64_1.tar.zstd".to_string(),
            full_url
        );
    }
}
