use std::{
    collections::{BTreeMap, btree_map::Entry},
    io,
    path::{Path, PathBuf},
    sync::OnceLock,
};

use anyhow::Context;

use git2::Repository;

use sequoia_openpgp as openpgp;
use openpgp::{
    Cert,
    Fingerprint,
    KeyHandle,
    armor,
    serialize::Marshal,
};

use sequoia_cert_store as cert_store;
use cert_store::Store;

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

const TRACE: bool = false;

pub fn dispatch(config: &Config, command: PolicySubcommand) -> Result<()> {
    let git_ = OnceLock::new();
    let git = || -> Result<&Repository, Error> {
        if let Some(repo) = git_.get() {
            return Ok(repo);
        }

        let repo = git_repo()?;

        let _ = git_.set(repo);
        Ok(git_.get().unwrap())
    };

    let git_policy = |git: &Repository, name: &str| -> Result<Policy> {
        let (object, reference) = git.revparse_ext(name)
            .with_context(|| {
                format!("Looking up {:?}.", name)
            })?;

        // We won't get a reference if we are passed an OID.
        let commit = if let Some(reference) = reference {
            let commit = reference.peel_to_commit()
                .with_context(|| {
                    format!("{:?} is not a commit", name)
                })?;

            commit.id()
        } else {
            if let Ok(commit) = object.into_commit() {
                commit.id()
            } else {
                return Err(anyhow::anyhow!("{:?} is not a commit", name));
            }
        };

        match Policy::read_from_commit(git, &commit) {
            Ok(policy) => Ok(policy),
            Err(err) => {
                if let Error::MissingPolicy(_) = err {
                    eprintln!("Warning {} does not have a policy file.  \
                               Defaulting to an empty policy.",
                              name);
                    Ok(Policy::default())
                } else {
                    Err(err.into())
                }
            }
        }
    };

    match command {
        PolicySubcommand::Describe {
            policy_file,
            commit,
        } => {
            let p = if let Some(commit) = commit {
                git_policy(git()?, &commit)?
            } else {
                config.read_policy(&policy_file)?
            };

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

        PolicySubcommand::Diff {
            old, old_commit, old_file, new, new_commit, new_file
        } => {
            tracer!(TRACE, "sq-git policy diff");

            let r = |commit: Option<&str>, file: Option<&Path>|
                -> Result<_>
            {
                let mut policy = None;
                let mut err_commit = None;
                if let Some(commit) = commit {
                    match git().and_then(|git| Ok(git_policy(git, &commit)?)) {
                        Ok(p) => {
                            t!("Read policy from commit {:?}", commit);
                            policy = Some(p)
                        },
                        Err(err) => {
                            t!("Error reading policy from commit {:?}: {}",
                               commit, err);
                            err_commit = Some(err);
                        }
                    }
                }

                let mut err_file = None;
                if policy.is_none() {
                    if let Some(file) = file {
                        match Policy::read_file(&file) {
                            Ok(p) => {
                                t!("Read policy from file {}", file.display());
                                policy = Some(p);
                            }
                            Err(err) => {
                                t!("Error reading policy from file {:?}: {}",
                                   file.display(), err);
                                err_file = Some(err)
                            }
                        }
                    }
                }

                match policy {
                    Some(policy) => Ok(policy),
                    None => {
                        if let (Some(commit), Some(err_commit))
                            = (commit, err_commit)
                        {
                            eprintln!("Reading commit {}: {}", commit, err_commit);
                        }
                        if let (Some(file), Some(err_file)) = (file, err_file)
                        {
                            eprintln!("Reading file {}: {}",
                                      file.display(), err_file);
                        }
                        Err(anyhow::anyhow!("Failed to read policy"))
                    }
                }
            };

            let old_commit = old_commit.as_deref().or(old.as_deref());

            let old_as_path = old.as_deref().map(|p| PathBuf::from(p));
            let old_file = old_file.as_deref().or(old_as_path.as_deref());

            let old_policy = if old_commit.is_some() || old_file.is_some() {
                r(old_commit, old_file)?
            } else {
                // Default to HEAD's policy.
                t!("Reading old policy from HEAD");
                git_policy(git()?, "HEAD")?
            };

            let new_commit = new_commit.as_deref().or(new.as_deref());

            let new_as_path = new.as_deref().map(|p| PathBuf::from(p));
            let new_file = new_file.as_deref().or(new_as_path.as_deref());

            let new_policy = if new_commit.is_some() || new_file.is_some() {
                r(new_commit, new_file)?
            } else {
                // Default to the policy in the working tree.
                t!("Reading new policy from git working tree");
                Policy::read_from_working_dir()?
            };

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

            if ! diff.changes.is_empty() {
                // Don't return an error: sq-git prints it, which is
                // annoying.
                std::process::exit(1);
            }
        },

        PolicySubcommand::Export {
            policy_file,
            commit,
            name,
            all,
        } => {
            let p = if let Some(commit) = commit {
                git_policy(git()?, &commit)?
            } else {
                config.read_policy(&policy_file)?
            };

            let mut certs: BTreeMap<Fingerprint, Cert> = BTreeMap::new();
            let mut merge = |cert: Cert| -> Result<()> {
                match certs.entry(cert.fingerprint()) {
                    Entry::Occupied(oe) => {
                        let oe = oe.into_mut();
                        *oe = (*oe).clone().merge_public_and_secret(cert)
                            .context("Merging certificates")?;
                    }
                    Entry::Vacant(ve) => {
                        ve.insert(cert);
                    }
                }

                Ok(())
            };

            if all {
                // Serialize all certificates.
                for (entity, a) in p.authorization().iter() {
                    for cert in a.certs()? {
                        let cert = cert.with_context(|| {
                            format!("Parsing {}'s keyring", entity)
                        })?;
                        let fpr = cert.fingerprint();
                        merge(Cert::try_from(cert).with_context(|| {
                            format!("Parsing {}'s keyring: {} is corrupted",
                                    entity, fpr)
                        })?)?;
                    }
                }
            } else if let Some(name) = name {
                if let Some(auth) = p.authorization().get(&name) {
                    // Only serialize the certificates for the specified
                    // entity.
                    for cert in auth.certs()? {
                        let cert = cert.with_context(|| {
                            format!("Parsing {}'s keyring", name)
                        })?;
                        let fpr = cert.fingerprint();
                        merge(Cert::try_from(cert).with_context(|| {
                            format!("Parsing {}'s keyring: {} is corrupted",
                                    name, fpr)
                        })?)?;
                    }
                } else {
                    eprintln!("Entity {:?} is not known.", name);
                    eprintln!("Known entities");
                    for name in p.authorization().keys() {
                        eprintln!("  - {}", name);
                    }
                    return Err(anyhow::anyhow!("Unknown entity"));
                }
            } else {
                unreachable!("enforced by clap");
            }

            let stdout = std::io::stdout();
            let mut output
                = armor::Writer::new(stdout, armor::Kind::PublicKey)?;
            for cert in certs.into_values() {
                cert.serialize(&mut output)?;
            }
            output.finalize()?;
        },

        PolicySubcommand::Authorize {
            policy_file,
            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_or_default(&policy_file)?;
            let mut p = old_policy.clone();

            let (new_entry, a) = match p.authorization_mut().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, &policy_file)?;
        },

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

            let mut keyserver = keyservers
                .into_iter()
                .map(|keyserver| {
                    sequoia_net::KeyServer::new(&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_packets(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_mut().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(certs) => {
                                            for c in certs {
                                                if let Ok(c) = 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, &policy_file)?;
        },

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

            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(&policy_file)?;
            let mut p = old_policy.clone();

            p.commit_goodlist_mut().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, &policy_file)?;
        },
    }

    Ok(())
}
