use std::borrow::Borrow;
use std::collections::HashMap;

use sequoia_openpgp as openpgp;
use openpgp::Fingerprint;
use openpgp::packet::UserID;

use crate::FULLY_TRUSTED;
use crate::Network;
use crate::Path;
use crate::Paths;
use crate::Root;
use crate::Roots;
use crate::network::filter::CapCertificateFilter;
use crate::network::filter::ChainFilter;
use crate::network::filter::SuppressCertificationFilter;
use crate::network::filter::SuppressIssuerFilter;
use crate::TRACE;

/// A certification network.
///
/// A `RootedNetwork` is a `Network` with a fixed set of roots.
pub struct RootedNetwork<'a> {
    // The underlying network.
    n: &'a Network,

    // The trust roots.
    roots: Roots,
}

impl<'a> RootedNetwork<'a> {
    /// New.
    pub fn new<R>(n: &'a Network, roots: R) -> Self
        where R: Into<Roots>
    {
        let mut roots = roots.into();

        tracer!(TRACE, "RootedNetwork::new");

        t!("Roots ({}): {}.",
           roots.iter().count(),
           roots.iter()
               .map(|r| format!("{} ({})", r.fingerprint(), r.amount()))
               .collect::<Vec<_>>()
               .join(", "));
        t!("Have {} nodes, {} have made at least one certification.",
           n.nodes.len(), n.edges.len());

        roots.retain(|r| {
            let fpr = r.fingerprint();
            if let Some(_) = n.nodes.get(&fpr) {
                true
            } else {
                t!("  Ignoring root that does not occur in network: {}",
                   fpr);
                false
            }
        });

        RootedNetwork {
            n,
            roots,
        }
    }

    /// Returns whether the specified certificate is a root.
    pub fn is_root<F>(&self, fpr: F) -> bool
        where F: Borrow<Fingerprint>
    {
        self.roots.is_root(fpr.borrow())
    }

    /// Returns the specified root.
    pub fn root<F>(&self, fpr: F) -> Option<&Root>
        where F: Borrow<Fingerprint>
    {
        self.roots.get(fpr.borrow())
    }

    /// Return a reference to the network.
    pub fn network(&self) -> &'a Network {
        self.n
    }

    /// Authenticates the specified binding.
    ///
    /// Enough independent paths are gotten to satisfy
    /// `target_trust_amount`.  A fully trusted authentication is 120.
    /// If you require that a binding be double authenticated, you can
    /// specify 240.
    pub fn authenticate<U, F>(&self, target_userid: U, target_fpr: F,
                              target_trust_amount: usize)
        -> Paths
    where U: Borrow<UserID>,
          F: Borrow<Fingerprint>,
    {
        tracer!(TRACE, "RootedNetwork::authenticate");

        let target_userid = target_userid.borrow();
        let target_fpr = target_fpr.borrow();

        t!("Authenticating <{}, {}>",
           target_fpr, String::from_utf8_lossy(target_userid.value()));
        t!("Roots ({}):", self.roots.iter().count());
        for (i, r) in self.roots.iter().enumerate() {
            t!("  {}: {} ({})", i, r.fingerprint(), r.amount());
        }

        let mut paths = Paths::new();

        let mut filter = ChainFilter::new();
        if self.roots.iter().any(|r| r.amount() != FULLY_TRUSTED) {
            let mut caps = CapCertificateFilter::new();
            for r in self.roots.iter() {
                let amount = r.amount();
                if amount != FULLY_TRUSTED  {
                    caps.cap(r.fingerprint().clone(), amount);
                }
            }
            filter.push(caps);
        };

        let mut progress = true;
        'next_path: while progress && paths.amount() < target_trust_amount {
            progress = false;

            for self_signed in [true, false] {
                let auth_paths: HashMap<Fingerprint, (Path, usize)>
                    = self.n.backward_propagate(
                        &self.roots, target_fpr.clone(), target_userid.clone(),
                        self_signed, &filter);

                // Note: the paths returned by backward_propagate may
                // overlap.  As such, we can only take one.  (Or we need
                // to subtract any overlap.  But that is fragile.)  Then
                // we subtract the path from the network and run
                // backward_propagate again, if necessary.
                if let Some((path, path_amount)) = self.roots.iter()
                    // Get the paths that start at the roots.
                    .filter_map(|r| {
                        auth_paths.get(&r.fingerprint())
                    })
                    // Choose the one that: has the maximum amount of
                    // trust.  If there are multiple such paths, prefer
                    // the shorter one.
                    .max_by_key(|(path, path_amount)| {
                        (// We want the *most* amount of trust,
                            path_amount,
                            // but the *shortest* path.
                            -(path.len() as isize),
                            // Be predictable.  Break ties based on the
                            // fingerprint of the root.
                            path.root().fingerprint())
                    })
                {
                    let path = path.clone();

                    if path.len() == 1 {
                        // It's a root.
                        let mut suppress_filter
                            = SuppressIssuerFilter::new();
                        suppress_filter.suppress_issuer(
                            &path.root().fingerprint(), *path_amount);
                        filter.push(suppress_filter);
                    } else {
                        // Add the path to the filter to create a residual
                        // network without this path.
                        let mut suppress_filter
                            = SuppressCertificationFilter::new();
                        suppress_filter.suppress_path(&path, *path_amount);
                        filter.push(suppress_filter);
                    }

                    paths.push(path, *path_amount);
                    progress = true;
                    // Prefer paths where the target User ID is self
                    // signed as long as possible.
                    continue 'next_path;
                }
            }
        }

        paths
    }
}
