use std::path::{
    PathBuf,
};

use clap::{
    ArgGroup,
    Parser,
};

// Note: this file is used both from main.rs and from build.rs.  This
// means that we can only use what is under the cli module!
//
// We also try to minimize the number of build dependencies to reduce
// the build time.  This includes not depending on sequoia-openpgp.
// Since we do need a couple of types from sequoia-openpgp, we mock
// them in build.rs.  If you import another type here, you'll need to
// mock that too.
use crate::openpgp;
use openpgp::{
    KeyHandle,
    // Mock additional imports in build.rs!
};

/// Returns the cert store's base directory.
pub fn cert_store_base() -> PathBuf {
    // XXX: openpgp-cert-d doesn't yet export this:
    // https://gitlab.com/sequoia-pgp/pgp-cert-d/-/issues/34
    // Remove this when it does.
    dirs::data_dir()
        .expect("Unsupported platform")
        .join("pgp.cert.d")
}

#[derive(Parser)]
#[command(
    author,
    name = "sq-git",
    version = format!("{}{}",
                      clap::crate_version!(),
                      if let Some(v) = option_env!("VERGEN_GIT_DESCRIBE") {
                          format!("-g{}", v)
                      } else {
                          "".into()
                      }),
    about = "A tool to help protect a project's supply chain.",
    long_about = "\
\"sq-git\" is a tool that can help improve a project's supply chain
security.

To use \"sq-git\", you add a policy file (\"openpgp-policy.toml\") to
the root of a \"git\" repository.  The policy file includes a list of
OpenPGP certificates, and the types of changes they are authorized to
make.  The capabilities include adding a commit, and authorizing a new
certificate.  See the \"sq-git init\" and \"sq-git policy\"
subcommands for more details.

A commit is considered authorized if the commit is signed, and at
least one immediate parent commit's policy authorizes the signer's
certificate to make that type of change.

A downstream user authenticates a version of the project using the
\"sq-git log\" subcommand.  They specify a trust root (a commit),
which they've presumably audited, and \"sq-git log\" looks for an
authenticated path from the trust root to the current \"HEAD\".  If
there is an authenticated path, then there is evidence that the
project's maintainers authorized all of the intermediate changes.

To find an authenticated path, \"sq-git\" starts with the current
commit, and tries to authenticate it using each of its parent commits.
It repeats this process for each parent commit that authenticated it.
If the trust root is reached, then the version is considered
authenticated.",
)]
pub struct Cli {
    #[clap(
        long,
        help = "Disables the use of a certificate store",
        long_help = format!("\
Disables the use of a certificate store.  By default, sq-git uses the \
user's standard cert-d, which is located in \"{}\".",
                            cert_store_base().display()),
    )]
    pub no_cert_store: bool,

    #[clap(
        long,
        value_name = "PATH",
        env = "SQ_CERT_STORE",
        conflicts_with_all = &[ "no_cert_store" ],
        help = "Specifies the location of the certificate store",
        long_help = format!("\
Specifies the location of the certificate store.  By default, sq-git \
uses the the user's standard cert-d, which is located in \"{}\".",
                            cert_store_base().display()),
    )]
    pub cert_store: Option<PathBuf>,

    /// Use POLICY instead of the repository's policy.
    ///
    /// The default policy is stored in the "openpgp-policy.toml"
    /// file in the root of the repository.
    #[arg(global = true, long, value_name = "POLICY")]
    pub policy_file: Option<PathBuf>,

    #[arg(
        global = true,
        long = "output-format",
        value_name = "FORMAT",
        value_parser = ["human-readable", "json"],
        default_value = "human-readable",
        help = "Produces output in FORMAT, if possible",
    )]
    pub output_format: String,

    #[command(subcommand)]
    pub subcommand: Subcommand,
}

#[derive(Parser, Debug)]
#[clap(
    name = "init",
    about = "Suggests how to create a policy",
    long_about = "\
Suggests how to create a policy by analyzing recent commits.  The
heuristic considers signed commits on the current branch that were
made over the past half year.  The heuristic suggests that the most
frequent committer be made the project maintainer, and other
committers be made committers.

Note: This is a *simple* heuristic; its recommendations should be
viewed as a starting point.  In particular, you still need to do some
due diligance.  It is essential that you review the suggested roles,
and check that people actually control the certificates.  Ideally, you
should ask each person for their OpenPGP fingerprint in person.  But
in the very least you should ask them via email.",

    after_help = "\
EXAMPLES:

# Inspects the current branch and suggests how to create a policy.
$ sq-git init",
)]
pub struct InitSubcommand {
    /// Show additional information.
    #[clap(
        short='v'
    )]
    pub verbose: bool,
}

/// Describe, update, and change the OpenPGP policy.
#[derive(clap::Subcommand)]
pub enum PolicySubcommand {
    /// Describes the policy.
    ///
    /// This reads in the policy default and dumps it in a more
    /// descriptive format on stdout.
    Describe {
    },

    /// Changes the authorizations.
    ///
    /// A certificate can delegate any of its capabilities to another
    /// certificate without breaking an authentication chain.
    ///
    /// To fork a project, you create a new policy file.
    Authorize {
        name: String,

        #[command(flatten)]
        cert: CertArg,

        /// Grant the certificate the sign-commit capability.
        ///
        /// This capability allows the certificate to sign commits.
        /// That is, when authenticating a version of the repository,
        /// a commit is considered authenticated if it is signed by a
        /// certificate with this capability.
        #[arg(long, overrides_with = "no_sign_commit",
              default_value_ifs(
                  [("committer", "true", Some("true")),
                   ("release_manager", "true", Some("true")),
                   ("project_maintainer", "true", Some("true"))]))
        ]
        sign_commit: bool,

        /// Rescind the sign-commit capability from a certificate.
        ///
        /// Removes the sign-commit capability for the certificate.
        /// Note: this operation is not retroactive; commits signed
        /// with the certificate, and made prior to the policy change
        /// are still considered authenticated.
        #[clap(long)]
        no_sign_commit: bool,

        /// Grant the certificate the sign-tag capability.
        ///
        /// This capability allows the certificate to sign tags.  That
        /// is, when authenticating a tag, a tag is considered
        /// authenticated if it is signed by a certificate with this
        /// capability.
        #[arg(long, overrides_with = "no_sign_tag",
              default_value_ifs(
                  [("release_manager", "true", Some("true")),
                   ("project_maintainer", "true", Some("true"))]))
        ]
        sign_tag: bool,
        /// Rescind the sign-tag capability from a certificate.
        ///
        /// Removes the sign-tag capability for the certificate.
        /// Note: this operation is not retroactive; tags signed with
        /// the certificate, and made prior to the policy change are
        /// still considered authenticated.
        #[clap(long)]
        no_sign_tag: bool,

        /// Grant the certificate the sign-archive capability.
        ///
        /// This capability allows the certificate to sign tarballs or
        /// other archives.  That is, when authenticating an archive,
        /// an archive is considered authenticated if it is signed by
        /// a certificate with this capability.
        #[arg(long, overrides_with = "no_sign_archive",
              default_value_ifs(
                  [("release_manager", "true", Some("true")),
                   ("project_maintainer", "true", Some("true"))]))
        ]
        sign_archive: bool,
        /// Rescind the sign-archive capability from a certificate.
        ///
        /// Removes the sign-archive capability for the certificate.
        /// Note: this operation is not retroactive; archives signed
        /// with the certificate prior to the policy change are still
        /// considered authenticated.
        #[clap(long)]
        no_sign_archive: bool,

        /// Grant the certificate the add-user capability.
        ///
        /// This capability allows the certificate add users to the
        /// policy file, and to grant them capabilities.  A
        /// certificate that has this capability is only allowed to
        /// grant capabilities that it has.  That is, if Alice has the
        /// "sign-commit" and "add-user" capability, she can grant Bob
        /// either of those capabilities, but she is can't grant him
        /// the "sign-tag" capability, because she does not have that
        /// capability.
        #[arg(long, overrides_with = "no_add_user",
              default_value_ifs(
                  [("project_maintainer", "true", Some("true"))]))
        ]
        add_user: bool,
        /// Rescind the add-user capability from a certificate.
        ///
        /// Removes the add-user capability for the certificate.
        /// Note: this operation is not retroactive; operations that
        /// rely on this grant prior to the policy change are still
        /// considered authenticated.
        ///
        /// Rescinding the add-user capability from a certificate does
        /// not rescind any grants that that certificate made.  That
        /// is, if Alice grants Bob the can-sign and add-user
        /// capability, Bob grants Carol the can-sign capability, and
        /// then Alice rescinds Bob's can-sign and add-user
        /// capabilities, Carol still has the can-sign capability.  In
        /// this way, a grant is a copy of a capability.
        #[clap(long)]
        no_add_user: bool,

        /// Grants the certificate the retire-user capability.
        ///
        /// This capability allows the certificate to rescind
        /// arbitrary capabilities.  That is, if Alice has the
        /// retire-user capability, she can rescind Bob's can-sign
        /// capability even if she didn't grant him that capability.
        #[arg(long, overrides_with = "no_retire_user",
              default_value_ifs(
                  [("project_maintainer", "true", Some("true"))]))
        ]
        retire_user: bool,
        /// Rescind the retire-user capability from a certificate.
        ///
        /// Removes the retire-user capability from a certificate.
        /// The specified certificate cannot no longer rescind
        /// capabilities even those that they granted.
        #[clap(long)]
        no_retire_user: bool,

        /// Grants the certificate the audit capability.
        ///
        /// This capability allows the certificate to audit commits.
        /// If Alice has the audit capability, Bob has the can-sign
        /// capability, and then Bob revokes his key, because it was
        /// compromised, then all commits that Bob signed are
        /// considered invalid.  Alice can recover from this situation
        /// by auditing Bob's commit.  After auditing each commit, she
        /// marks it as good.
        #[arg(long, overrides_with = "no_audit",
              default_value_ifs(
                  [("project_maintainer", "true", Some("true"))]))
        ]
        audit: bool,
        /// Rescind the audit capability from a certificate.
        ///
        /// Removes the audit capability from a certificate.  The
        /// specified certificate cannot no longer mark arbitrary
        /// commits as good.
        #[clap(long)]
        no_audit: bool,

        /// Grants all capabilities relevant to a project maintainer.
        ///
        /// A project maintainer is a person who is responsible for
        /// maintaining the project.  This options grants the
        /// certificate all capabilities.
        #[arg(long)]
        project_maintainer: bool,

        /// Grants all capabilities relevant to a release manager.
        ///
        /// A release manager is authorized to commit changes, and
        /// make releases.  This options grants the certificate the
        /// "sign-tag", "sign-archive", and "sign-commit"
        /// capabilities.
        #[arg(long)]
        release_manager: bool,

        /// Grants all capabilities relevant to a committer.
        ///
        /// A committer is authorized to commit changes to the code.
        /// This options grants the certificate the "sign-commit"
        /// capability.
        #[arg(long)]
        committer: bool,
    },

    /// Updates the OpenPGP certificates in the policy.
    ///
    /// "sq-git" looks for updates to the certificates listed in the
    /// policy file in the user's certificate store, and on
    /// the main public keyservers.
    ///
    /// # Examples
    ///
    /// # Download updates to the specified certificate from WKD to
    /// # the local certificate store.
    /// $ sq wkd get neal@walfield.org
    ///
    /// # Look for updates.
    /// $ sq-git policy sync
    Sync {
        /// Looks for updates on the specified keyservers.
        ///
        /// In addition to looking in the local certificate store,
        /// also looks for updates in the specified keyserver.
        #[clap(
            long, short='s',
            default_values_t = [
                "hkps://keys.openpgp.org".to_string(),
                "hkps://keyserver.ubuntu.com".to_string(),
                "hkps://api.protonmail.ch".to_string(),
            ],
        )]
        keyserver: Vec<String>,

        /// Don't look for updates on any keyservers.
        #[arg(long)]
        disable_keyservers: bool,
    },

    /// Adds the given commit to the commit goodlist.
    ///
    /// This requires the audit capability to not break an
    /// authentication chain.
    Goodlist {
        commit: String,
    },
}

#[derive(clap::Subcommand)]
pub enum Subcommand {
    Init(InitSubcommand),

    Policy {
        #[command(subcommand)]
        command: PolicySubcommand,
    },

    /// Lists and verifies commits.
    ///
    /// Lists and verifies that the commits from the given trust root
    /// to the target commit adhere to the policy.
    ///
    /// A version is considered authenticated if there is a path from
    /// the trust root to the target commit on which each commit can
    /// be authenticated by its parent.
    ///
    /// If the key used to sign a commit is hard revoked, then the
    /// commit is considered bad.  "sq-git" looks for hard revocations
    /// in all of the commits that it examines.  Thus, if a project
    /// maintainer adds a hard revocation to a commit's policy file,
    /// it will cause later *and* earlier commits signed with that key
    /// to be considered invalid.  This is useful when a key has been
    /// compromised.
    ///
    /// When a key has been hard revoked, downstream users either need
    /// to start using a more recent trust root, or the upstream
    /// project maintainers need to audit the relevant commits.  If
    /// the commits are considered benign, they can be added to a
    /// goodlist using "sq-git policy goodlist".  When a commit is
    /// considered authenticated, but the certificate has been hard
    /// revoked, "sq-git" looks to see whether the commit has been
    /// goodlisted by a commit that is on an authenticated path from
    /// the commit in question to the target.  If so, the commit is
    /// considered to be authenticated.
    Log {
        /// Specifies the trust root.
        ///
        /// If no policy is specified, then the value of the
        /// repository's "sequoia.trust-root" configuration key is
        /// used as the trust root.
        #[arg(long, value_name = "COMMIT")]
        trust_root: Option<String>,

        /// Continues to check commits even when it is clear that the
        /// version cannot be authenticated.
        ///
        /// Causes "sq-git" to continue to check commits rather than
        /// stopping as soon as it is clear that the version can't be
        /// authenticated.
        #[arg(long)]
        keep_going: bool,

        /// After authenticating the current version, prunes the
        /// certificates.
        ///
        /// After authenticating the current version, prunes unused
        /// components of the certificates.  In particular, subkeys
        /// that were not used to verify a signature, and User IDs
        /// that were never considered primary are removed.
        ///
        /// This does not remove unused certificates from the policy
        /// file; this just minimizes them.
        #[arg(long)]
        prune_certs: bool,

        /// The commits to check.
        ///
        /// If not specified, HEAD is authenticated with repsect to
        /// the trust root.
        ///
        /// If a single commit id is specified, the specified commit
        /// is authenticated with respect to the trust root.
        ///
        /// If a commit range like "3895a3a..3b388ae" is specified,
        /// the end of the range is authenticated with repsect to the
        /// trust root, and there must be an authenticated path from
        /// the trust root via the start of the range to the end of
        /// the range.
        commit_range: Option<String>,
    },

    /// Verifies signatures on archives like release tarballs.
    Verify {
        /// Read the policy from this commit.  Falls back to using the
        /// value of the repository's "sequoia.trust-root"
        /// configuration key.  Can be overridden using
        /// "--policy-file".
        #[arg(long, value_name = "COMMIT")]
        trust_root: Option<String>,

        /// The signature to verify.
        #[arg(long, value_name = "FILENAME")]
        signature: PathBuf,

        /// The archive that the signature protects.
        #[arg(long, value_name = "FILENAME")]
        archive: PathBuf,
    },

    /// git update hook that enforces an OpenPGP policy.
    ///
    /// Insert the following line into "hooks/update" on the shared
    /// git server to make it enforce the policy embedded in the
    /// repository starting at the given trust root "COMMIT".
    ///
    ///     sq-git update-hook --trust-root=<COMMIT> "$@"
    ///
    /// When a branch is pushed that is not previously known to the
    /// server, sq-git checks that all commits starting from the trust
    /// root to the pushed commit adhere to the policy.
    ///
    /// When a branch is pushed that is previously known to the
    /// server, i.e. the branch is updated, sq-git checks that
    /// all new commits starting from the commit previously known to
    /// the server to the pushed commit adhere to the policy.  If
    /// there is no path from the previously known commit to the new
    /// one, the branch has been rebased.  Then, we fall back to
    /// searching a path from the trust root.
    UpdateHook {
        /// The commit to use as a trust root.
        #[arg(long, value_name = "COMMIT", required = true)]
        trust_root: String,

        /// The name of the ref being updated (supplied as first
        /// argument to the update hook, see githooks(5)).
        ref_name: String,

        /// The old object name stored in the ref (supplied as second
        /// argument to the update hook, see githooks(5)).
        old_object: String,

        /// The new object name stored in the ref (supplied as third
        /// argument to the update hook, see githooks(5)).
        new_object: String,
    }
}

#[derive(clap::Args, Debug)]
#[clap(group(ArgGroup::new("cert")
             .args(&["value", "cert_handle", "cert_file"])
             .required(true)))]
pub struct CertArg {
    /// The filename, fingerprint or Key ID of the certificate to
    /// authenticate.
    ///
    /// This is first interpreted as a filename.  If that file does
    /// not exist, then it is interpreted as a fingerprint or Key ID,
    /// and read from the certificate store as described for the CERT
    /// argument.  To avoid ambiguity, use "--cert" or "--cert-file".
    #[arg(value_name="FILE|FINGERPRINT|KEYID")]
    pub value: Option<String>,

    /// The fingerprint or Key ID of the certificate to use..
    ///
    /// This is read from the user's default certificate
    /// directory.  On Unix-like systems, this is usually located
    /// in "$HOME/.local/share/pgp.cert.d".
    #[arg(long="cert", value_name="FINGERPRINT|KEYID")]
    pub cert_handle: Option<KeyHandle>,

    /// The file containing the certificate to authorize.
    ///
    /// The file must contain exactly one certificate.
    #[arg(long, value_name="FILE")]
    pub cert_file: Option<PathBuf>,
}
