use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
use url::Url;

#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct Certificates {
    /// All known server certificates
    #[serde(rename = "certificate")]
    #[serde(default = "default_entries")]
    pub entries: HashMap<String, String>,
}

fn default_entries() -> HashMap<String, String> {
    HashMap::<String, String>::new()
}

impl Certificates {
    pub fn new() -> Certificates {
        let confdir = Certificates::get_known_hosts_filename();
        let mut config_string = String::new();
        if Path::new(confdir.as_str()).exists() {
            config_string = std::fs::read_to_string(&confdir).unwrap_or_default();
        }
        // println!("Could not read known_hosts file: {}", e);

        toml::from_str(&config_string).unwrap_or_default()
    }

    fn get_known_hosts_filename() -> String {
        let confdir: String = match dirs::config_dir() {
            Some(mut dir) => {
                dir.push(env!("CARGO_PKG_NAME"));
                dir.push("known_hosts");
                dir.into_os_string().into_string().unwrap()
            }
            None => String::new(),
        };
        info!("Looking for known_hosts file {}", confdir);
        confdir
    }

    /// Add or replace the fingerprint that would be used for the given
    /// normalized URL.
    pub fn insert(&mut self, url: &Url, fingerprint: String) {
        let id = Certificates::extract_domain_port(url);
        info!("Adding entry to known_hosts: {} = {}", id, fingerprint);

        self.entries.insert(id, fingerprint);
        if let Err(why) = self.write_to_file() {
            warn!("Could not write known_hosts to file: {}", why)
        }
    }

    /// Retrieve the fingerprint that fits the domain of the given
    /// normalized URL.
    ///
    /// Returns None if the URL does not have a domain or the
    /// host has not been visited before.
    pub fn get(&mut self, url: &Url) -> Option<String> {
        let id = Certificates::extract_domain_port(url);
        info!("Looking for fingerprint for {}", id);
        self.entries.get(&id).cloned()
    }

    pub fn write_to_file(&mut self) -> std::io::Result<()> {
        let filename = Certificates::get_known_hosts_filename();
        info!("Saving known_hosts to file: {}", filename);
        // Create a path to the desired file
        let path = Path::new(&filename);

        let mut file = std::fs::File::create(path)?;

        file.write_all(b"# Automatically generated by ncgopher.\n")?;
        file.write_all(
            toml::to_string(&self)
                .expect("known hosts could not be stored as TOML")
                .as_bytes(),
        )?;
        Ok(())
    }

    /// Reduce a URL to the relevant part for fingerprinting: There may be
    /// different certificates for
    /// * different IP adresses or (sub)domains
    /// * different ports on the same (sub)domain
    fn extract_domain_port(url: &Url) -> String {
        let host = url.host_str().expect("gemini URL without host");
        if let Some(port) = url.port() {
            // assumes that URL has been normalized before
            format!("{}:{}", host, port)
        } else {
            host.to_string()
        }
    }
}
