#[macro_use] mod log;

use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fs::File;

use anyhow::Context;

use sequoia_openpgp as openpgp;
use openpgp::Cert;
use openpgp::KeyID;
use openpgp::Fingerprint;
use openpgp::KeyHandle;
use openpgp::Result;
use openpgp::packet::UserID;
use openpgp::policy::StandardPolicy;

use sequoia_openpgp_mt as openpgp_mt;
use openpgp_mt::keyring;

use sequoia_wot as wot;

mod cli;
mod gpg;

const TRACE: bool = false;

fn authenticate(r: wot::RootedNetwork, m: &clap::ArgMatches,
                userid: bool, certificate: bool)
    -> Result<()>
{
    let amount: usize = if let Some(v) = m.value_of("trust-amount") {
        v.parse()?
    } else if m.is_present("full") {
        wot::FULLY_TRUSTED
    } else if m.is_present("partial") {
        wot::PARTIALLY_TRUSTED
    } else if m.is_present("double") {
        2 * wot::FULLY_TRUSTED
    } else {
        wot::FULLY_TRUSTED
    };

    let userid = if userid {
        Some(UserID::from(m.value_of("userid").expect("required")))
    } else {
        None
    };

    let fingerprint: Option<Fingerprint> = if certificate {
        let cert = m.value_of("cert").expect("required");
        let kh: KeyHandle = cert.parse()
            .with_context(|| {
                format!(
                    "The CERT argument must be a valid Fingerprint or Key ID \
                     (got: {:?})",
                    cert)
            })?;
        Some(match kh {
            KeyHandle::Fingerprint(fpr) => fpr,
            kh @ KeyHandle::KeyID(_) => {
                let mut fpr = None;
                for c in r.network().certs() {
                    if kh.aliases(KeyHandle::from(c.fingerprint())) {
                        fpr = Some(c.fingerprint());
                        break;
                    }
                }

                if let Some(fpr) = fpr {
                    fpr
                } else {
                    println!("{} does not appear in the network",
                             kh);
                    std::process::exit(1);
                }
            }
        })
    } else {
        None
    };

    let email = m.is_present("email");

    let mut bindings = Vec::new();
    if matches!(userid, Some(_)) && email {
        let userid = userid.as_ref().expect("required");

        // First, we check that the supplied User ID is a bare
        // email address.
        let email = String::from_utf8(userid.value().to_vec())
            .context("email address must be valid UTF-8")?;

        let userid_check = UserID::from(format!("<{}>", email));
        if let Ok(Some(email_check)) = userid_check.email() {
            if email != email_check {
                println!("{:?} does not appear to be an email address",
                         email);
                std::process::exit(1);
            }
        } else {
            println!("{:?} does not appear to be an email address",
                     email);
            std::process::exit(1);
        }

        // Now, iterate over all of the certifications of the target,
        // and select the bindings where the User ID matches the email
        // address.
        bindings = if let Some(fingerprint) = fingerprint.as_ref() {
            r.network().certified_userids_of(fingerprint.clone())
                .into_iter()
                .map(|userid| (fingerprint.clone(), userid))
                .collect::<Vec<_>>()
        } else {
            r.network().certified_userids()
        };

        let email_normalized = userid_check.email_normalized()
            .expect("checked").expect("checked");
        bindings = bindings.into_iter()
            .filter_map(|(fingerprint, userid_other)| {
                if let Ok(Some(email_other_normalized))
                    = userid_other.email_normalized()
                {
                    if email_normalized == email_other_normalized {
                        Some((fingerprint, userid_other.clone()))
                    } else {
                        None
                    }
                } else {
                    None
                }
            }).collect();
    } else if let Some(fingerprint) = fingerprint {
        if let Some(userid) = userid.as_ref() {
            bindings.push((fingerprint, userid.clone()));
        } else {
            // Fingerprint, no User ID.
            bindings = r.network().certified_userids()
                .into_iter()
                .filter(|(fingerprint_other, _userid_other)| {
                    &fingerprint == fingerprint_other
                }).collect();
        }
    } else if let Some(userid) = userid.as_ref() {
        // The caller did not specify a certificate.  Find all
        // bindings with the User ID.
        bindings = r.network().certified_userids()
            .into_iter()
            .filter(|(_fingerprint, userid_other)| {
                userid_other == userid
            })
            .collect();
    } else {
        // No User ID, no Fingerprint.
        // List everything.

        bindings = r.network().certified_userids();

        if let Some(pattern) = m.value_of("pattern") {
            // Or rather, just User IDs that match the pattern.
            let pattern = pattern.to_lowercase();

            bindings = bindings
                .into_iter()
                .filter(|(_fingerprint, userid)| {
                    if email {
                        // Compare with the normalized email address,
                        // and the raw email address.
                        if let Ok(Some(email)) = userid.email_normalized() {
                            // A normalized email is already lowercase.
                            if email.contains(&pattern) {
                                return true;
                            }
                        }

                        if let Ok(Some(email)) = userid.email() {
                            if email.to_lowercase().contains(&pattern) {
                                return true;
                            }
                        }

                        return false;
                    } else if let Ok(userid)
                        = std::str::from_utf8(userid.value())
                    {
                        userid.to_lowercase().contains(&pattern)
                    } else {
                        // Ignore User IDs with invalid UTF-8.
                        false
                    }
                })
                .collect();
        }
    };

    if bindings.len() == 0 {
        println!("No matching certifications.");

        // Perhaps the caller specified an email address, but forgot
        // to add --email.  If --email is not present and the
        // specified User ID looks like an email, try and be helpful.
        if ! m.is_present("email") {
            if let Some(userid) = userid {
                if let Ok(email) = std::str::from_utf8(userid.value()) {
                    let userid_check = UserID::from(format!("<{}>", email));
                    if let Ok(Some(email_check)) = userid_check.email() {
                        if email == email_check {
                            println!("WARNING: {} appears to be a bare \
                                      email address.  Perhaps you forgot \
                                      to specify --email.",
                                     email);
                        }
                    }
                }
            }
        }

        std::process::exit(1);
    }

    // There may be multiple certifications of the same
    // User ID.  Dedup.
    bindings.sort();
    bindings.dedup();


    let mut authenticated = false;
    for (fingerprint, userid) in bindings.iter() {
        let paths = r.authenticate(userid.clone(),
                                   fingerprint.clone(), amount);

        let a = paths.amount();
        if a == 0 {
            continue;
        }

        if a >= amount {
            authenticated = true;
        }

        println!("[{}] {} {}: {} authenticated ({}%)",
                 if a >= amount { "✓" } else { " " },
                 fingerprint, String::from_utf8_lossy(userid.value()),
                 if a >= 2 * wot::FULLY_TRUSTED { "doubly" }
                 else if a >= wot::FULLY_TRUSTED { "fully" }
                 else if a >= wot::PARTIALLY_TRUSTED { "partially" }
                 else { "marginally" },
                 (a * 100) / wot::FULLY_TRUSTED);

        for (i, (path, amount)) in paths.iter().enumerate() {
            let prefix = if paths.len() > 1 {
                println!("  Path #{} of {}, trust amount {}:",
                         i + 1, paths.len(), amount);
                "    "
            } else {
                "  "
            };

            let certification_count = path.certifications().count();

            print!("{}◯ {}", prefix, path.root().fingerprint());
            if certification_count == 0 {
                print!(" {:?}", String::from_utf8_lossy(userid.value()));
            } else if let Some(userid) = path.root().primary_userid() {
                print!(" ({:?})",
                       String::from_utf8_lossy(userid.value()));
            }
            println!("");

            for (last, c) in path.certifications().enumerate()
                .map(|(j, c)| {
                    if j + 1 == certification_count {
                        (true, c)
                    } else {
                        (false, c)
                    }
                })
            {
                let cert = c.target();

                print!("{}│  ", prefix);
                if c.amount() < wot::FULLY_TRUSTED {
                    print!(" partially certified (amount: {} of 120)",
                           c.amount());
                } else {
                    print!(" certified");
                }
                if last {
                    print!(" the following binding");
                } else {
                    print!(" the following certificate");
                }

                print!(" on {}",
                       chrono::DateTime::<chrono::Utc>::from(
                           c.creation_time()).format("%Y-%m-%d"));
                if let Some(e) = c.expiration_time() {
                    print!(" (expiry: {})",
                           chrono::DateTime::<chrono::Utc>::from(
                               e).format("%Y-%m-%d"));
                }
                if c.depth() > 0.into() {
                    print!(" as a");
                    if c.amount() != wot::FULLY_TRUSTED {
                        print!(" partially trusted ({} of 120)", c.amount());
                    } else {
                        print!(" fully trusted");
                    }
                    if c.depth() == 1.into() {
                        print!(" introducer (depth: {})",
                               c.depth());
                    } else {
                        print!(" meta-introducer (depth: {})",
                               c.depth());
                    }
                }
                println!("");

                print!("{}{} {}",
                       prefix,
                       if last { "└" } else { "├" },
                       cert.fingerprint());

                if last {
                    print!(" {:?}",
                           String::from_utf8_lossy(userid.value()));
                } else {
                    if let Some(userid) = cert.primary_userid() {
                        print!(" ({:?})",
                               String::from_utf8_lossy(userid.value()));
                    }
                }
                println!("");
            }

            println!("");
        }
    }

    if ! authenticated {
        std::process::exit(1);
    }

    Ok(())
}


fn main() -> Result<()> {
    tracer!(TRACE, "sq-wot");

    let policy = &mut StandardPolicy::new();

    let matches = cli::build().get_matches();

    // We check whether a trust root is specified here instead of
    // using Clap::ArgGroup, as we are able to provide the user with
    // more helpful feedback when that is not the case.
    if !(matches.is_present("trust-root")
         || matches.is_present("gpg-ownertrust")
         || matches.is_present("gpg"))
    {
        eprintln!("\
No trust roots specified.  Specify one or more trust roots using
`--trust-root` or use `--gpg-ownertrust` or `--gpg` to read the
trust roots from GnuPG.");
        std::process::exit(1);
    }

    // Likewise for keyrings.
    if !(matches.is_present("keyring")
         || matches.is_present("gpg-keyring")
         || matches.is_present("gpg"))
    {
        eprintln!("\
No keyrings specified.  Specify one or more keyring using `--keyring`
or use `--gpg-keyring` or `--gpg` to read GnuPG's keyring.");
        std::process::exit(1);
    }


    let known_notations: Vec<&str> = matches.values_of("known-notation")
        .unwrap_or_default()
        .collect();
    policy.good_critical_notations(&known_notations);

    let mut trust_roots: Vec<(KeyHandle, usize)> = Vec::new();
    if let Some(args) = matches.values_of("trust-root") {
        for arg in args {
            let kh: KeyHandle = arg.parse()?;
            trust_roots.push((kh, wot::FULLY_TRUSTED));
        }
    }

    let mut other_roots: Vec<(Fingerprint, usize)> = Vec::new();

    if matches.is_present("gpg-ownertrust")
        || matches.is_present("gpg")
    {
        let ownertrust = gpg::export_ownertrust()?;
        for (fpr, ownertrust) in ownertrust {
            match ownertrust {
                gpg::OwnerTrust::Ultimate => {
                    trust_roots.push((fpr.into(), wot::FULLY_TRUSTED));
                },
                gpg::OwnerTrust::Fully => {
                    other_roots.push((fpr, wot::FULLY_TRUSTED));
                },
                gpg::OwnerTrust::Marginal => {
                    other_roots.push((fpr, wot::PARTIALLY_TRUSTED));
                },
                _ => (),
            }
        }
    }

    let mut certs: Vec<Cert> = Vec::new();
    if let Some(args) = matches.values_of_os("keyring") {
        for filename in args {
            let file = File::open(filename)
                .context(format!("Parsing {:?}", filename))
                .unwrap();

            let some_certs = keyring::parse(file)?
                .into_iter()
                .filter_map(|cert| {
                    match cert {
                        Ok(cert) => Some(cert),
                        Err(err) => {
                            eprintln!("Warning: while parsing {:?}: {}",
                                      filename, err);
                            None
                        }
                    }
                });

            certs.extend(some_certs);
        }
    }

    if matches.is_present("gpg-keyring")
        || matches.is_present("gpg")
    {
        let keyring = gpg::export()?;
        let some_certs = keyring::parse(std::io::Cursor::new(keyring))?
            .into_iter()
            .filter_map(|cert| {
                match cert {
                    Ok(cert) => Some(cert),
                    Err(err) => {
                        eprintln!("Warning: while parsing gpg's keyring: {}",
                                  err);
                        None
                    }
                }
            });

        certs.extend(some_certs);
    }

    // Map any Key IDs to Fingerprints.
    let mut keyid_index: Option<HashMap<KeyID, Vec<Fingerprint>>> = None;
    let mut trust_roots: Vec<(Fingerprint, usize)> = trust_roots
        .into_iter()
        .filter_map(|(kh, amount)| {
            match kh {
                KeyHandle::Fingerprint(fpr) => Some((fpr, amount)),
                KeyHandle::KeyID(keyid) => {
                    let i = match keyid_index.as_ref() {
                        Some(i) => i,
                        None => {
                            // Initialize the hash map lazily.
                            let mut i: HashMap<KeyID, Vec<Fingerprint>>
                                = HashMap::new();
                            for c in certs.iter() {
                                match i.entry(c.keyid()) {
                                    e @ Entry::Occupied(_) => {
                                        e.and_modify(|e| {
                                            e.push(c.fingerprint())
                                        });
                                    }
                                    e @ Entry::Vacant(_) => {
                                        e.or_insert(
                                            vec![ c.fingerprint() ]);
                                    }
                                }
                            }

                            keyid_index = Some(i);
                            keyid_index.as_ref().unwrap()
                        }
                    };

                    if let Some(fprs) = i.get(&keyid) {
                        if fprs.len() > 1 {
                            let fprs: Vec<String>
                                = fprs.iter().map(|f| f.to_hex()).collect();
                            eprintln!("\
Key ID ({}) matches multiple certificates: {}",
                                      keyid, fprs.join(", "));
                            std::process::exit(1);
                        }
                        Some((fprs[0].clone(), amount))
                    } else {
                        t!("Ignoring root {}: not in network.", keyid);
                        None
                    }
                }
            }
        })
        .collect();

    let reference_time = std::time::SystemTime::now();

    let n = wot::Network::from_certs(
        certs.into_iter(), policy, reference_time)?;

    let mut r = wot::RootedNetwork::new(&n, &trust_roots[..]);

    let mut other_trust_roots: Vec<(Fingerprint, usize)> = Vec::new();
    if ! other_roots.is_empty() {
        // For GnuPG to consider a non-ultimately trusted root as
        // valid, there must be a path from an ultimately trusted root
        // to the non-ultimately trusted root.  If this is the case,
        // add those roots.

        'root: for (other_root, amount) in &other_roots {
            let cert = match n.cert(other_root) {
                None => {
                    t!("Ignoring root {}: not in network.", other_root);
                    continue;
                }
                Some(cert) => cert,
            };

            for u in cert.userids() {
                if u.revocation_status().in_effect(reference_time) {
                    t!("Ignoring root {}'s User ID {:?}: revoked.",
                       other_root, String::from_utf8_lossy(u.value()));
                    continue;
                }

                let authenticated_amount = r.authenticate(
                    u.userid(), other_root, wot::FULLY_TRUSTED).amount();

                if authenticated_amount >= wot::FULLY_TRUSTED {
                    // Authenticated!  We'll keep it.
                    t!("Non-ultimately trusted root <{}, {}> reachable, \
                        keeping at {}",
                       other_root,
                       String::from_utf8_lossy(u.userid().value()),
                       *amount);
                    other_trust_roots.push((other_root.clone(), *amount));
                    continue 'root;
                } else {
                    t!("Non-ultimately trusted binding <{}, {}> \
                        NOT fully trusted (amount: {})",
                       other_root,
                       String::from_utf8_lossy(u.userid().value()),
                       authenticated_amount);
                }
            }

            t!("Non-ultimately trusted root {} NOT fully trusted. Ignoring.",
               other_root);
        }
    }

    if ! other_trust_roots.is_empty() {
        trust_roots.extend(other_trust_roots);
        r = wot::RootedNetwork::new(&n, &trust_roots[..]);
    }

    match matches.subcommand() {
        ("authenticate",  Some(m)) => {
            // Authenticate a given binding.
            authenticate(r, m, true, true)?;
        }

        ("lookup",  Some(m)) => {
            // Find all authenticated bindings for a given
            // User ID, list the certificates.
            authenticate(r, m, true, false)?;
        }

        ("identify",  Some(m)) => {
            // Find and list all authenticated bindings for a given
            // certificate.
            authenticate(r, m, false, true)?;
        }

        ("list",  Some(m)) => {
            // List all authenticated bindings.
            authenticate(r, m, false, false)?;
        }

        _ => unreachable!(),
    }

    Ok(())
}
