use std::{
    collections::btree_map::Entry,
    io,
};

use anyhow::Context;

use sequoia_openpgp as openpgp;
use openpgp::{
    Cert,
    KeyHandle,
};

use sequoia_cert_store as cert_store;
use cert_store::Store;

use crate::Config;
use crate::Result;
use crate::cli::PolicySubcommand;
use crate::git_repo;
use crate::output;

pub fn dispatch(config: &Config, command: PolicySubcommand) -> Result<()> {
    match command {
        PolicySubcommand::Describe {
        } => {
            let p = config.read_policy()?;

            match config.output_format {
                output::Format::HumanReadable => {
                    output::describe_policy(&p)?;
                },
                output::Format::Json =>
                    serde_json::to_writer_pretty(io::stdout(), &p)?,
            }
        },

        PolicySubcommand::Authorize {
            name, cert,
            sign_commit, no_sign_commit,
            sign_tag, no_sign_tag,
            sign_archive, no_sign_archive,
            add_user, no_add_user,
            retire_user, no_retire_user,
            audit, no_audit,
            project_maintainer: _,
            release_manager: _,
            committer: _,
        } => {
            let cert = cert.get(&config)?;
            let fp = cert.fingerprint();

            let old_policy = config.read_policy()?;
            let mut p = old_policy.clone();

            let (new_entry, a) = match p.authorization.entry(name) {
                Entry::Occupied(oe) => (false, oe.into_mut()),
                Entry::Vacant(ve) => (true, ve.insert(Default::default())),
            };

            if new_entry
                && (! sign_commit && ! sign_tag && ! sign_archive
                    && ! add_user && ! retire_user && ! audit)
            {
                eprintln!("Warning: Adding new entry with no capabilities.  \
                           You probably want to add some capabilities by \
                           running the command again, and specifying \
                           \"--committer\", \"--release-manager\", \
                           or \"--project-maintainer\".  Refer to the \
                           help for details.\"");
            }

            let mut merged = false;
            let mut updated = Vec::new();
            for c in a.certs()? {
                let mut c = Cert::try_from(c?)?;
                if c.fingerprint() == fp {
                    c = c.merge_public(cert.clone())?;
                    merged = true;
                }
                updated.push(c);
            }
            if ! merged {
                updated.push(cert);
            }
            a.set_certs(updated)?;

            a.sign_commit =
                (a.sign_commit | sign_commit) & !no_sign_commit;
            a.sign_tag =
                (a.sign_tag | sign_tag) & !no_sign_tag;
            a.sign_archive =
                (a.sign_archive | sign_archive) & !no_sign_archive;

            a.add_user =
                (a.add_user | add_user) & !no_add_user;
            a.retire_user =
                (a.retire_user | retire_user) & !no_retire_user;

            a.audit =
                (a.audit | audit) & !no_audit;

            let diff = old_policy.diff(&p)?;
            match config.output_format {
                output::Format::HumanReadable => {
                    output::describe_diff(&diff)?;
                },
                output::Format::Json =>
                    serde_json::to_writer_pretty(io::stdout(), &diff)?,
            }

            config.write_policy(&p)?;
        },

        PolicySubcommand::Sync {
            keyserver: keyservers,
            disable_keyservers,
        } => {
            let old_policy = config.read_policy()?;
            let mut p = old_policy.clone();

            let mut keyserver = keyservers
                .into_iter()
                .map(|keyserver| {
                    sequoia_net::KeyServer::new(
                        sequoia_net::Policy::Encrypted,
                        &keyserver)
                        .map(|instance| {
                            (keyserver, instance)
                        })
                })
                .collect::<sequoia_net::Result<Vec<_>>>()?;

            let cert_update = |cert: Cert, update: Cert| -> (Cert, bool) {
                if cert.fingerprint() != update.fingerprint() {
                    eprintln!("bad server response, \
                               wrong certificate ({}).",
                              update.fingerprint());
                    (cert, false)
                } else {
                    match cert.clone().insert_packets2(update.into_packets()) {
                        Ok((cert, changed)) => {
                            if changed {
                                eprintln!("updated.");
                            } else {
                                eprintln!("unchanged.");
                            }

                            (cert, changed)
                        }
                        Err(err) => {
                            eprintln!("{}", err);
                            (cert, false)
                        }
                    }
                }
            };

            // XXX: We should do this in parallel.
            tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .unwrap()
                .block_on(async {
                    let mut any_changed = false;
                    for (id, a) in p.authorization.iter_mut() {
                        let mut changed = false;
                        let mut updated = Vec::new();
                        for cert in a.certs()? {
                            let mut cert = Cert::try_from(cert?)?;
                            let fp = cert.fingerprint();
                            eprint!("Updating {} ({}) from the local \
                                     certificate store... ",
                                    fp, id);
                            if let Ok(c) = config.cert_store()?
                                .lookup_by_cert_fpr(&fp)
                                .and_then(|lc| lc.to_cert().cloned())
                            {
                                let did_change;
                                (cert, did_change) = cert_update(cert, c);
                                changed |= did_change;
                            } else {
                                eprintln!("not found.");
                            };

                            if ! disable_keyservers {
                                let kh = KeyHandle::from(fp);
                                for (uri, keyserver) in keyserver.iter_mut() {
                                    eprint!("Updating {} ({}) from {}... ",
                                            kh, id, uri);
                                    match keyserver.get(kh.clone()).await {
                                        Ok(c) => {
                                            let did_change;
                                            (cert, did_change) = cert_update(cert, c);
                                            changed |= did_change;
                                        }
                                        Err(err) => {
                                            eprintln!("{}.", err);
                                        }
                                    }
                                }
                            }

                            updated.push(cert);
                        }
                        if changed {
                            a.set_certs(updated)?;
                            any_changed = true;
                        }
                    }

                    if any_changed {
                        eprintln!("Note: certificates are stripped so not \
                                   all certificate updates may be relevant.");
                    }

                    Ok::<(), anyhow::Error>(())
                })?;

            let diff = old_policy.diff(&p)?;
            match config.output_format {
                output::Format::HumanReadable => {
                    output::describe_diff(&diff)?;
                },
                output::Format::Json =>
                    serde_json::to_writer_pretty(io::stdout(), &diff)?,
            }

            config.write_policy(&p)?;
        },

        PolicySubcommand::Goodlist {
            commit,
        } => {
            let git = git_repo()?;

            let object = git.revparse_single(&commit)
                .with_context(|| {
                    format!("Looking up \"{}\"", commit)
                })?;

            let commit = object.peel_to_commit()
                .with_context(|| {
                    format!("\"{}\" does not refer to a commit",
                            commit)
                })?;

            let old_policy = config.read_policy()?;
            let mut p = old_policy.clone();

            p.commit_goodlist.insert(commit.id().to_string());

            let diff = old_policy.diff(&p)?;
            match config.output_format {
                output::Format::HumanReadable => {
                    output::describe_diff(&diff)?;
                },
                output::Format::Json =>
                    serde_json::to_writer_pretty(io::stdout(), &diff)?,
            }

            config.write_policy(&p)?;
        },
    }

    Ok(())
}
