/// Collects statistics about the SKS packet dump using the openpgp
/// crate, Sequoia's low-level API.
///
/// Note that to achieve reasonable performance, you need to compile
/// Sequoia and this program with optimizations:
///
///     % cargo run --example dsa-statistics --release \
///           -- <packet-dump> [<packet-dump>..]

use std::{
    env,
    fs,
    sync::mpsc::sync_channel,
    thread,
    time::Duration,
};

use anyhow::Context;

use sequoia_openpgp as openpgp;
use openpgp::{
    cert::amalgamation::ValidAmalgamation,
    types::*,
    policy::NullPolicy,
};
use sequoia_openpgp_mt::keyring;

fn main() -> openpgp::Result<()> {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        return Err(anyhow::anyhow!("Collects statistics about OpenPGP packet dumps.\n\n\
                Usage: {} <packet-dump> [<packet-dump>...]\n", args[0]));
    }

    let (sender, receiver) = sync_channel(4);

    // For each input file, create a parser.
    let n_threads = num_cpus::get() / 2;
    let chunk_size = args[1..].len() / n_threads;
    for inputs in args[1..].chunks(chunk_size) {
        let inputs = inputs.to_vec();
        let sender = sender.clone();
        thread::spawn(move || {
            for input in inputs {
                eprintln!("Parsing {:?}...", input);
                let f = fs::File::open(&input)
                    .context(format!("Failed to parse {:?}", input))
                    .unwrap();
                sender.send(keyring::parse(f)
                            .context(format!("Failed to parse {:?}", input))
                            .unwrap()).unwrap();
            }
        });
    }
    drop(sender);

    const P: &NullPolicy = unsafe { &NullPolicy::new() };
    let five_years = Duration::new(5 * 365 * 24 * 60 * 60, 0);

    let mut all_certs_including_parse_errors = 0;

    let mut all_certs = 0;
    let mut gp_certs = 0;

    let mut valid_all_certs = 0;
    let mut valid_gp_certs = 0;
    let mut valid_gp_dsa_elg_certs = 0;

    let mut valid_recent_all_certs = 0;
    let mut valid_recent_gp_certs = 0;
    let mut valid_recent_gp_dsa_elg_certs = 0;

    fn is_revoked(s: RevocationStatus) -> bool {
        match s {
            RevocationStatus::Revoked(_) => true,
            _ => false,
        }
    }

    while let Ok(certs) = receiver.recv() {
        all_certs_including_parse_errors += certs.len();

        for cert in certs.into_iter().filter_map(|c| c.ok()) {
            all_certs += 1;

            // Canonicalize at creation time to see if it once had an
            // encryption (sub)key.
            let creation_time = cert.primary_key().key().creation_time();
            if let Ok(vcert0) = cert.with_policy(P, creation_time) {
                if vcert0.keys()
                    .for_transport_encryption()
                    .for_storage_encryption()
                    .count() > 0
                {
                    gp_certs += 1;
                }
            }

            // Canonicalize for now.
            if let Ok(vcert) = cert.with_policy(P, None) {
                if ! vcert.primary_key().alive().is_ok()
                    || ! is_revoked(vcert.primary_key().revocation_status())
                {
                    // Primary not alive or revoked.
                    continue;
                }

                let recent =
                    if cert.primary_key().key().creation_time().elapsed().unwrap()
                    <= five_years { 1 } else { 0 };

                valid_all_certs += 1;
                valid_recent_all_certs += recent;

                // General purpose now?
                if vcert.keys()
                    .for_transport_encryption()
                    .for_storage_encryption()
                    .alive()
                    .revoked(false)
                    .count() > 0
                {
                    valid_gp_certs += 1;
                    valid_recent_gp_certs += recent;
                }

                #[allow(deprecated)]
                if vcert.primary_key().key().pk_algo() == PublicKeyAlgorithm::DSA
                    && vcert.keys()
                    .for_transport_encryption()
                    .for_storage_encryption()
                    .alive()
                    .revoked(false)
                    .filter(|vka|
                            vka.key().pk_algo() == PublicKeyAlgorithm::ElGamalEncrypt)
                    .count() > 0
                {
                    valid_gp_dsa_elg_certs += 1;
                    valid_recent_gp_dsa_elg_certs += recent;
                }
            }
        }
    }

    println!("All (including parse errors): {}",
             all_certs_including_parse_errors);

    println!("All certs: {} ({:.2}% parsed ok)",
             all_certs,
             all_certs as f32 / all_certs_including_parse_errors as f32 * 100.);

    println!("General purpose certs (GP): {} ({:.2}% of all certs)",
             gp_certs,
             gp_certs as f32 / all_certs as f32 * 100.);

    println!("Valid now certs: {} ({:.2}% of all certs)",
             valid_all_certs,
             valid_all_certs as f32 / all_certs as f32 * 100.);

    println!("Valid now GP certs: {} ({:.2}% of all valid now certs)",
             valid_gp_certs,
             valid_gp_certs as f32 / valid_all_certs as f32 * 100.);

    println!("Valid now GP DSA/ELG certs: {} ({:.2}% of all valid now GP certs)",
             valid_gp_dsa_elg_certs,
             valid_gp_dsa_elg_certs as f32 / valid_gp_certs as f32 * 100.);

    println!("Valid now recent certs: {} ({:.2}% of all valid now certs)",
             valid_recent_all_certs,
             valid_recent_all_certs as f32 / valid_all_certs as f32 * 100.);

    println!("Valid now recent GP certs: {} ({:.2}% of all valid now recent certs)",
             valid_recent_gp_certs,
             valid_recent_gp_certs as f32 / valid_recent_all_certs as f32 * 100.);

    println!("Valid now recent GP DSA/ELG certs: {} ({:.2}% of all valid now recent GP certs)",
             valid_recent_gp_dsa_elg_certs,
             valid_recent_gp_dsa_elg_certs as f32 / valid_recent_gp_certs as f32 * 100.);

    Ok(())
}
