use std::{
    env,
    io,
    path::{
        Path,
        PathBuf,
    }
};

use anyhow::{anyhow, Context, Result};

use clap::FromArgMatches;

use once_cell::sync::OnceCell;

use sequoia_openpgp::{
    self as openpgp,
    Cert,
    KeyHandle,
    parse::Parse,
};
use sequoia_cert_store::{
    CertStore,
    Store,
};
use sequoia_git::*;

#[macro_use]
mod macros;

mod cli;
use cli::CertArg;
use cli::PolicyFileArg;
use cli::paths::StateDirectory;

mod commands;
mod output;
#[allow(dead_code)]
mod utils;

impl CertArg {
    fn get(&self, config: &Config) -> Result<Cert> {
        let filename;
        let r: Result<(&Path, Vec<u8>), KeyHandle> = if let Some(value) = &self.value {
            // First try to open as a file.  Only if the file does not
            // exist, interpret the value as a key handle.

            filename = PathBuf::from(value);
            match std::fs::read(&filename) {
                Ok(contents) => {
                    Ok((&filename, contents))
                }
                Err(err) => {
                    if err.kind() == std::io::ErrorKind::NotFound {
                        match value.parse::<KeyHandle>() {
                            Ok(kh) => Err(kh),
                            Err(err) => return Err(
                                err.context(
                                    format!("File {} does not exist, \
                                             and is not a valid fingerprint \
                                             or Key ID",
                                            filename.display()))),
                        }
                    } else {
                        return Err(anyhow::Error::from(err).context(
                            format!("Opening {}", filename.display())));
                    }
                }
            }
        } else if let Some(kh) = &self.cert_handle {
            Err(kh.clone())
        } else if let Some(filename) = &self.cert_file {
            let content = std::fs::read(&filename)
                .with_context(|| {
                    format!("Opening {}", filename.display())
                })?;
            Ok((filename, content))
        } else {
            unreachable!("clap ensures that one argument is set")
        };

        match r {
            Ok((filename, content)) => {
                // Parse content as a Cert and make sure content only
                // contains a single certificate.
                Cert::from_bytes(&content)
                    .with_context(|| {
                        format!("Parsing {}", filename.display())
                    })
            }
            Err(kh) => {
                let certs = config.cert_store()?.lookup_by_cert_or_subkey(&kh)?;

                let cert = match certs.len() {
                    0 => return Err(anyhow!("Key {} not found", kh)),
                    1 => certs[0].to_cert()?.clone(),
                    n => return Err(anyhow!(
                        "Key {} is part of {} certs, use cert \
                         fingerprint to resolve",
                        kh, n)),
                };

                Ok(cert)
            }
        }
    }
}

pub struct Config<'a> {
    output_format: output::Format,
    home: Option<sequoia_directories::Home>,
    cert_store_path: Option<StateDirectory>,
    cert_store: OnceCell<CertStore<'a>>,
}

impl<'a> Config<'a> {
    /// Returns the cert store's base directory, if it is enabled.
    fn cert_store_base(&self) -> Option<PathBuf> {
        let default = || if let Ok(path) = std::env::var("PGP_CERT_D") {
            Some(PathBuf::from(path))
        } else {
            self.home.as_ref()
                .map(|h| h.data_dir(sequoia_directories::Component::CertD))
        };

        if let Some(state) = self.cert_store_path.as_ref() {
            match state {
                StateDirectory::Absolute(p) => Some(p.clone()),
                StateDirectory::Default => default(),
                StateDirectory::None => None,
            }
        } else {
            default()
        }
    }

    // Returns the cert store.
    //
    // If it is not yet open, opens it.
    //
    // If it does not exist, issues a warning and returns an empty
    // cert store.
    fn cert_store(&self) -> Result<&CertStore<'a>> {
        if let Some(cert_store) = self.cert_store.get() {
            // The cert store is already initialized, return it.
            return Ok(cert_store);
        }

        let create_dirs = |path: &Path| -> Result<()> {
            use std::fs::DirBuilder;

            let mut b = DirBuilder::new();
            b.recursive(true);

            // Create the parent with the normal umask.
            if let Some(parent) = path.parent() {
                // Note: since recursive is turned on, it is not an
                // error if the directory exists, which is exactly
                // what we want.
                b.create(parent)
                    .with_context(|| {
                        format!("Creating the directory {:?}", parent)
                    })?;
            }

            // Create path with more restrictive permissions.
            platform!{
                unix => {
                    use std::os::unix::fs::DirBuilderExt;
                    b.mode(0o700);
                },
                windows => {
                },
            }

            b.create(path)
                .with_context(|| {
                    format!("Creating the directory {:?}", path)
                })?;

            Ok(())
        };

        // We need to initialize the cert store.

        // Open the cert-d.

        let path = self.cert_store_base()
            .expect("just checked that it is configured");

        let cert_store = create_dirs(&path)
            .and_then(|_| CertStore::open(&path))
            .with_context(|| {
                format!("While opening the certificate store at {:?}",
                        &path)
            })?;

        let _ = self.cert_store.set(cert_store);

        Ok(self.cert_store.get().expect("just configured"))
    }

    fn read_policy<'f, F>(&self, policy_file: F) -> Result<Policy>
    where
        F: Into<Option<&'f PolicyFileArg>>,
    {
        let policy_file = policy_file.into();
        let policy_file = if let Some(policy_file) = policy_file.as_ref() {
            policy_file.path.as_ref()
        } else {
            None
        };

        if let Some(path) = policy_file {
            Policy::read_file(path)
                .with_context(|| {
                    format!("Reading specified policy file: {}",
                            path.display())
                })
        } else {
            Policy::read_from_working_dir()
                .with_context(|| {
                    format!("Reading default policy file")
                })
        }
    }

    fn read_policy_or_default<'f, F>(&self, policy_file: F) -> Result<Policy>
    where
        F: Into<Option<&'f PolicyFileArg>>,
    {
        let policy_file = policy_file.into();
        let policy_file = if let Some(policy_file) = policy_file.as_ref() {
            policy_file.path.as_ref()
        } else {
            None
        };

        if let Some(path) = policy_file {
            Policy::read_file_or_default(path)
                .with_context(|| {
                    format!("Reading specified policy file: {}",
                            path.display())
                })
        } else {
            Policy::read_from_working_dir()
                .with_context(|| {
                    format!("Reading default policy file")
                })
        }
    }

    fn write_policy<'f, F>(&self, p: &Policy, policy_file: F) -> Result<()>
    where
        F: Into<Option<&'f PolicyFileArg>>,
    {
        let policy_file = policy_file.into();
        let policy_file = if let Some(policy_file) = policy_file.as_ref() {
            policy_file.path.as_ref()
        } else {
            None
        };

        if let Some(path) = policy_file {
            p.write(path)
                .with_context(|| {
                    format!("Updating the specified policy file: {}",
                            path.display())
                })
        } else {
            p.write_to_working_dir()
                .with_context(|| {
                    format!("Updating default policy file")
                })
        }
    }
}

// Returns the current git repository.
fn git_repo() -> Result<git2::Repository> {
    let cwd = env::current_dir()
        .context("Getting current working directory")?;
    let repo = git2::Repository::discover(&cwd)
        .with_context(|| {
            format!("Looking for git repository under {}",
                    cwd.display())
        })?;

    Ok(repo)
}

fn main() -> anyhow::Result<()> {
    let policy = openpgp::policy::StandardPolicy::new(); // XXX

    let mut cli = cli::build(true);
    let matches = cli.clone().try_get_matches();
    let mut matches = match matches {
        Ok(matches) => matches,
        Err(mut err) => {
            // Warning: hack ahead!
            //
            // We want to hide global options in the help output for
            // subcommands, and we want to include values from the
            // configuration file in the help output.
            //
            // If we are showing the help output, we only want to
            // display the global options at the top-level; for
            // subcommands we hide the global options to not overwhelm
            // the user.
            //
            // Ideally, clap would provide a mechanism to only show
            // the help output for global options at the level they
            // are defined at.  That's not the case.
            //
            // We can use `err` to figure out if we are showing the
            // help output, but it doesn't tell us what subcommand we
            // are showing the help for.  Instead (and here's the
            // hack!), we compare the output.  If it is the output for
            // the top-level `--help` or `-h`, then we are showing the
            // help for the top-level.  If not, then we are showing
            // the help for a subcommand.  In the former case, we
            // unhide the global options.
            use clap::error::ErrorKind;
            if err.kind() == ErrorKind::DisplayHelp
                || err.kind() == ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
            {
                let output = err.render();
                let output = if output == cli.render_long_help() {
                    Some(cli::build(false).render_long_help())
                } else if output == cli.render_help() {
                    Some(cli::build(false).render_help())
                } else {
                    // Redo the parse so that the help message will
                    // include any augmentations.
                    err = cli::build(true).try_get_matches().unwrap_err();
                    None
                };

                if let Some(output) = output {
                    if err.use_stderr() {
                        eprint!("{}", output);
                    } else {
                        print!("{}", output);
                    }
                    std::process::exit(err.exit_code());
                }
            }

            // Print the error message.
            err.print()?;

            // Then, figure out if this is a usage message, and
            // extract the usage string.
            if let Some(usage) = err.context()
                .find_map(|(kind, value)|
                          (kind == clap::error::ContextKind::Usage)
                          .then_some(value))
            {
                print_examples(&cli, usage)?;
            }

            // Finally, exit with an error code.
            std::process::exit(err.exit_code());
        }
    };
    let cli = cli::Cli::from_arg_matches_mut(&mut matches)?;

    let home = match &cli.home {
        Some(StateDirectory::Absolute(p)) =>
            Some(sequoia_directories::Home::new(p.clone())?),
        None | Some(StateDirectory::Default) =>
            Some(sequoia_directories::Home::default()
                 .ok_or(anyhow::anyhow!("no default SEQUOIA_HOME \
                                         on this platform"))?
                 .clone()),
        Some(StateDirectory::None) => None,
    };

    let config = Config {
        output_format: cli.output_format.parse()?,
        home,
        cert_store_path: cli.cert_store,
        cert_store: Default::default(),
    };

    let commit_by_symbolic_name = |git: &git2::Repository, name: &str,
                                   trust_root: bool|
        -> Result<git2::Oid>
    {
        // Allow the zero oid.
        if let Ok(oid) = git2::Oid::from_str(name) {
            if oid.is_zero() {
                return Ok(oid);
            }
        }

        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.
        if let Some(reference) = reference {
            if trust_root {
                if reference.is_tag() {
                    eprintln!("Warning: using a tag as the trust root \
                               could allow the remote repository to \
                               manipulate your trust root.");
                } else if reference.is_remote() {
                    eprintln!("Warning: using a remote branch as the \
                               trust root could allow the remote repository \
                               to manipulate your trust root.");
                }
            }

            let commit = reference.peel_to_commit()
                .with_context(|| {
                    format!("{:?} is not a commit", name)
                })?;

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

    let lookup_trust_root = |git: &git2::Repository,
                             trust_root: Option<&str>|
        -> Result<git2::Oid>
    {
        if let Some(trust_root) = trust_root {
            return commit_by_symbolic_name(git, trust_root, true);
        }

        // We only look in the repository's configuration file.
        let config = git.config()?
            .open_level(git2::ConfigLevel::Local)?
            .snapshot()?;
        let trust_root = match config.get_str("sequoia.trustRoot") {
            Ok(trust_root) => trust_root,
            Err(err) => {
                if err.code() == git2::ErrorCode::NotFound {
                    eprintln!("Warning: no trust root specified.  Either \
                               pass the '--trust-root' option, or set \
                               the 'sequoia.trustRoot' configuration \
                               key in your repository's local git config \
                               to reference a commit.");
                    if config.get_str("sequoia.trust-root").is_ok() {
                        eprintln!("Warning: you seem to have set the \
                                   `sequoia.trust-root` key.  This key \
                                   has been deprecated.");
                    }
                }

                return Err(anyhow::Error::from(err).context(
                    "Reading 'sequoia.trustRoot' from the repository's \
                     git config."));
            }
        };

        commit_by_symbolic_name(git, trust_root, true)
    };

    match cli.subcommand {
        cli::Subcommand::Init(command) => {
            commands::init::dispatch(command)?;
        }

        cli::Subcommand::Policy { command } => {
            commands::policy::dispatch(&config, command)?;
        }

        cli::Subcommand::Log {
            policy_file,
            trust_root,
            keep_going,
            prune_certs,
            commit_range,
        } => {
            if prune_certs && commit_range.is_some()
                && policy_file.path.is_none()
            {
                return Err(anyhow!("--prune-certs can only modify \
                                    HEAD or a shadow policy"));
            }

            let git = git_repo()?;
            let trust_root = lookup_trust_root(&git, trust_root.as_deref())?;

            let shadow_p = if let Some(s) = &policy_file.path {
                Some(std::fs::read(s)?)
            } else {
                None
            };
            let shadow_p = shadow_p.as_deref();

            let head = git.head()?.target().unwrap();
            let (start, target) = if let Some(commit_range) = commit_range {
                let mut s = commit_range.splitn(2, "..");
                let first = s.next().expect("always one component");
                if let Some(second) = s.next() {
                    if second.is_empty() {
                        (commit_by_symbolic_name(&git, first, false)?, head)
                    } else {
                        (commit_by_symbolic_name(&git, first, false)?,
                         commit_by_symbolic_name(&git, second, false)?)
                    }
                } else {
                    (trust_root, commit_by_symbolic_name(&git, first, false)?)
                }
            } else {
                (trust_root, head)
            };

            let mut cache = VerificationCache::new()?;

            let mut vresults = VerificationResult::default();
            let result = match config.output_format {
                output::Format::HumanReadable => {
                    verify(&git, trust_root, shadow_p,
                           (start, target),
                           &mut vresults,
                           keep_going,
                           |oid, parent_oid, result| {
                               output::Commit::new(
                                   &git, oid, parent_oid,
                                   &policy_file.path, result)?
                                   .describe(&mut io::stdout())?;
                               Ok(())
                           },
                           &mut cache,
                    )
                },
                output::Format::Json => {
                    use serde::ser::{Serializer, SerializeSeq};
                    let mut serializer = serde_json::ser::Serializer::pretty(
                        std::io::stdout());
                    let mut seq = serializer.serialize_seq(None)?;
                    let r =
                        verify(&git, trust_root, shadow_p,
                               (start, target),
                               &mut vresults,
                               keep_going,
                               |oid, parent_oid, result| {
                                   seq.serialize_element(
                                       &output::Commit::new(
                                           &git, oid, parent_oid,
                                           &policy_file.path, result)?
                                   ).map_err(anyhow::Error::from)?;
                                   Ok(())
                               },
                               &mut cache,
                        );
                    seq.end()?;
                    r
                },
            };

            if prune_certs {
                let mut p = config.read_policy(&policy_file)?;

                for a in p.authorization_mut().values_mut() {
                    let certs =
                        a.certs()?
                        .map(|r| r.and_then(Cert::try_from))
                        .collect::<Result<Vec<_>>>()?;

                    a.set_certs_filter(
                        certs,
                        // Keep all subkeys that made a signature, and
                        // those that are alive now.
                        |sk| {
                            let fp = sk.key().fingerprint();
                            vresults.signer_keys.contains(&fp)
                                || {
                                    // Slightly awkward, because we
                                    // cannot use sk.with_policy.
                                    let c = sk.cert();

                                    c.with_policy(&policy, None)
                                        .map(|vka| vka.keys().key_handle(fp)
                                             .next().is_some())
                                        .unwrap_or(false)
                                }
                        },
                        // Keep all user IDs that were primary user
                        // IDs when a signature was made, and the ones
                        // that are the primary userid now.
                        |uid| vresults.primary_uids.contains(uid.userid())
                            || {
                                // Slightly awkward, because we
                                // cannot use sk.with_policy.
                                let c = uid.cert();

                                c.with_policy(&policy, None)
                                    .and_then(|vka| vka.primary_userid())
                                    .map(|u| u.userid() == uid.userid())
                                    .unwrap_or(false)
                            }
                    )?;
                }

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

            let _ = cache.persist();
            result?;
        },

        cli::Subcommand::Verify {
            policy_file,
            trust_root,
            signature,
            archive,
        } => {
            let git = git_repo()?;
            let policy = if let Some(s) = policy_file.path.as_ref() {
                Policy::read_file(s)
                    .with_context(|| {
                        format!("Reading specified policy file: {}",
                                s.display())
                    })?
            } else {
                let trust_root = lookup_trust_root(
                    &git, trust_root.as_deref())?;

                Policy::read_from_commit(&git, &trust_root)
                    .with_context(|| {
                        format!("Reading policy from commit {}",
                                trust_root)
                    })?
            };

            // XXX: In the future, mmap the data.
            let signature = std::fs::read(&signature)
                .with_context(|| {
                    format!("Reading signature data from {}",
                            signature.display())
                })?;
            let archive = std::fs::read(&archive)
                .with_context(|| {
                    format!("Reading archive data from {}",
                            archive.display())
                })?;

            let r = policy.verify_archive(signature, archive);
            let o = output::Archive::new(r)?;
            match config.output_format {
                output::Format::HumanReadable =>
                    o.describe(&mut io::stdout())?,
                output::Format::Json =>
                    serde_json::to_writer_pretty(io::stdout(), &o)?,
            }
        },

        cli::Subcommand::UpdateHook {
            policy_file,
            trust_root,
            ref_name: _,
            old_object,
            new_object,
        } => {
            let git = git_repo()?;
            let trust_root = commit_by_symbolic_name(&git, &trust_root, true)
                .with_context(|| {
                    format!("Looking up specified trust root ({})",
                            trust_root)
                })?;
            let new_object = commit_by_symbolic_name(&git, &new_object, false)
                .with_context(|| {
                    format!("Looking up new object ({})",
                            new_object)
                })?;
            let old_object = commit_by_symbolic_name(&git, &old_object, false)
                .with_context(|| {
                    format!("Looking up old object ({})",
                            old_object)
                })?;

            // Fall back to the trust root if this is a new branch.
            let start = if let Err(err)
                = git_is_ancestor(&git, old_object, new_object)
            {
                if let Some(e) = err.downcast_ref::<sequoia_git::Error>() {
                    if matches!(e, Error::NoPathConnecting(_, _)) {
                        // There's no path from old object to new
                        // object.  Use the trust root.
                        trust_root
                    } else {
                        // Some other error: abort.
                        return Err(err);
                    }
                } else {
                    // There's a path from old object to new object.
                    old_object
                }
            } else {
                trust_root
            };

            let mut cache = VerificationCache::new()?;
            let mut vresults = VerificationResult::default();
            let result = verify(&git, trust_root,
                   None,
                   (start, new_object),
                   &mut vresults,
                   false,
                   |oid, parent_oid, result| {
                       output::Commit::new(
                           &git, oid, parent_oid, &policy_file.path, result)?
                           .describe(&mut io::stdout())?;
                       Ok(())
                   },
                   &mut cache,
            );

            let _ = cache.persist();
            result?;
        },
    }
    Ok(())
}

/// Given a `clap::Command` and a usage string, try to augment the
/// usage message with relevant examples.
fn print_examples(cli: &clap::Command, usage: impl ToString) -> Result<()> {
    // First, find the invoked subcommand.
    let binary = "sq-git";
    let usage = usage.to_string();
    let prefix = usage.find(&format!("{} ", binary));
    let subcommands = if let Some(i) = prefix {
        usage[i + binary.len() + 1..].split(" ")
        // Split, but only take the parts that do not denote options
        // or flags.
            .take_while(|p| p.chars().all(|c| c.is_alphabetic()))
    } else {
        // Odd...
        return Ok(());
    };

    // Now traverse the CLI tree to find the subcommand.
    let mut cmd = cli;
    for sub in subcommands {
        cmd = if let Some(c) = cmd.get_subcommands()
            .find(|c| c.get_name() == sub)
        {
            c
        } else {
            // Very odd...
            return Ok(());
        };
    }

    // And print the examples, if any.
    if let Some(examples) = cmd.get_after_help() {
        eprintln!("\n{}", examples);
    }

    Ok(())
}

#[cfg(test)]
mod test {
    use super::*;

    // Run some clap tests.
    #[test]
    fn verify_app() {
        let cli = cli::build(true);
        cli.debug_assert();
    }
}
