#![allow(unused)]

use std::{
    collections::{BTreeMap, BTreeSet},
    ffi::OsString,
    fs::File,
    io::Write,
    process::{Child, Command, Output, Stdio},
    path::Path,
    path::PathBuf,
    sync::{Arc, OnceLock},
    time::{Duration, SystemTime},
};

use anyhow::anyhow;

use sequoia_openpgp::{
    Fingerprint,
    Packet,
    cert::{
        Cert,
        CertBuilder,
        CertParser,
        CertRevocationBuilder,
        KeyBuilder,
        SubkeyRevocationBuilder,
        amalgamation::ValidAmalgamation,
    },
    packet::Signature,
    parse::Parse,
    policy::StandardPolicy,
    serialize::Serialize,
    types::{
        KeyFlags,
        ReasonForRevocation,
        RevocationStatus,
    }
};
use sequoia_cert_store::{
    StoreUpdate,
    store::certd::CertD,
};

pub type Result<T, E=Error> = std::result::Result<T, E>;

const P: &StandardPolicy = &StandardPolicy::new();

pub struct Identity {
    pub email: &'static str,
    pub petname: &'static str,
    pub fingerprint: Fingerprint,
    pub cert: Cert,
    pub rev: Signature,
}

impl Identity {
    fn new(email: &'static str, petname: &'static str) -> Result<Identity> {
        let (cert, rev) =
            CertBuilder::general_purpose(Some(format!("<{}>", email)))
            .set_creation_time(SystemTime::now() - Duration::new(24 * 3600, 0))
            .generate()?;

        Ok(Identity {
            email,
            petname,
            fingerprint: cert.fingerprint(),
            cert,
            rev,
        })
    }

    /// Returns the certificate with the pregenerated hard revocation.
    #[allow(dead_code)]
    pub fn hard_revoke(&self) -> Cert {
        self.cert.clone().insert_packets(self.rev.clone())
            .expect("ok").0
    }
}


/// Rotate a certificate's signing subkey.
///
/// Retires all of the certificate's signing capable subkeys.
/// Adds a new signing-capable subkey.
pub fn rotate_subkeys(cert: &Cert) -> Cert {
    let vc = cert.with_policy(P, None).expect("valid cert");

    // Revoke the old subkeys.
    let mut signer = cert.primary_key().key().clone()
        .parts_into_secret().unwrap()
        .into_keypair().unwrap();
    let mut packets = Vec::new();
    for sk in vc.keys().subkeys().for_signing() {
        if let RevocationStatus::Revoked(_) = sk.revocation_status() {
            // Already revoked.
            continue;
        }

        let sig = SubkeyRevocationBuilder::new()
            .set_reason_for_revocation(ReasonForRevocation::KeyRetired,
                                       b"Retired").unwrap()
            .build(&mut signer, &cert, sk.key(), None).unwrap();
        packets.push(Packet::from(sk.key().clone()));
        packets.push(Packet::from(sig));
    }

    // Add a new signing subkey.
    let cert2 = KeyBuilder::new(KeyFlags::signing())
        .subkey(vc.clone()).unwrap()
        .attach_cert().unwrap();

    // Merge everything
    if packets.is_empty() {
        cert2
    } else {
        cert2.insert_packets(packets).expect("can insert packets").0
    }
}

/// Revokes the certificate.
pub fn revoke_cert(cert: &Cert, reason: ReasonForRevocation) -> Cert {
    let vc = cert.with_policy(P, None).expect("valid cert");

    // Revoke the old subkeys.
    let mut signer = cert.primary_key().key().clone()
        .parts_into_secret().unwrap()
        .into_keypair().unwrap();

    let sig = CertRevocationBuilder::new()
        .set_reason_for_revocation(reason, b"revoked").unwrap()
        .build(&mut signer, &cert, None).unwrap();

    let cert = cert.clone().insert_packets(sig.clone())
        .expect("can insert revocation certificate").0;

    // Now it's revoked.
    assert_eq!(RevocationStatus::Revoked(vec![&sig]),
               cert.revocation_status(P, None));

    cert
}

pub enum TempDir {
    TempDir(tempfile::TempDir),
    PathBuf(PathBuf),
}

impl TempDir {
    fn new() -> Result<Self> {
        Ok(TempDir::TempDir(tempfile::TempDir::new()?))
    }

    fn path(&self) -> &Path {
        match self {
            TempDir::TempDir(d) => d.path(),
            TempDir::PathBuf(p) => p.as_path(),
        }
    }

    fn persist(&mut self) {
        let d = std::mem::replace(self, TempDir::PathBuf(PathBuf::new()));

        match d {
            TempDir::TempDir(d) => *self = TempDir::PathBuf(d.into_path()),
            TempDir::PathBuf(p) => *self = TempDir::PathBuf(p),
        }
    }
}

pub struct Environment {
    // If not None, then faketime is used.
    pub time: Option<SystemTime>,
    pub wd: TempDir,
    pub willow: Identity,
    pub willow_release: Identity,
    pub buffy: Identity,
    pub xander: Identity,
    pub riley: Identity,
}

// Whether we have faketime.
//
// Ok(true): yes
// Ok(false): skip faketime tests
// Err(err): no
static HAVE_FAKETIME: OnceLock<std::result::Result<bool, Error>>
    = OnceLock::new();

impl Environment {
    /// Before using `at`, `time`, or `tick`, we need to check for
    /// faketime.
    ///
    /// If this returns `Ok(false)`, the test should be silently
    /// skipped.
    pub fn check_for_faketime() -> std::result::Result<bool, &'static Error> {
        let r = HAVE_FAKETIME.get_or_init(|| {
            if let Ok(val) = std::env::var("NO_FAKETIME") {
                return Ok(false);
            }

            let mut cmd = Command::new("faketime");
            cmd.env("TZ", "UTC");

            let t = chrono::DateTime::<chrono::Utc>::from(SystemTime::now())
                .format("%Y-%m-%d %H:%M:%S")
                .to_string();
            cmd.arg("-f").arg(t);

            // We rely on being able to run git.
            cmd.arg("git").arg("--version");

            let output = cmd
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn().map_err(anyhow::Error::from)?
                .wait_with_output().map_err(anyhow::Error::from)?;

            eprintln!("stdout:{}\nstderr:{}\n",
                      String::from_utf8_lossy(&output.stdout),
                      String::from_utf8_lossy(&output.stderr));

            if output.status.success() {
                Ok(true)
            } else {
                Err(Error::NoFaketime(output))
            }
        });

        // Convert &Result<bool, Error> to Result<bool, &Error>.
        match r {
            Ok(b) => Ok(*b),
            Err(err) => Err(err)
        }
    }

    /// Be sure to call `Self::check_for_faketime` to make sure using
    /// a fake time is supported.
    pub fn at<T>(t: T) -> Result<Environment>
    where T: Into<Option<SystemTime>>
    {
        let t = t.into();

        if t.is_some() {
            assert!(HAVE_FAKETIME.get().is_some(),
                    "You forgot to call Environment::check_for_faketime \
                     before calling Environment::at");
            eprintln!("Using faketime.");
        } else {
            eprintln!("Using wall clock.");
        }

        let e = Environment {
            time: t,
            wd: TempDir::new()?,
            willow: Identity::new("willow@scoobies.example",
                                  "Willow Rosenberg Code Signing")?,
            willow_release: Identity::new("willow@scoobies.example",
                                          "Willow Rosenberg Release Signing")?,
            buffy: Identity::new("buffy@scoobies.example", "Buffy Summers")?,
            xander: Identity::new("xander@scoobies.example", "Xander Harris")?,
            riley: Identity::new("riley@scoobies.example", "Riley Finn")?,
        };

        let create_dir = |path: &Path| {
            eprintln!("Creating {}", path.display());
            std::fs::create_dir(path)
        };

        create_dir(&e.gnupg_state())?;
        create_dir(&e.git_state())?;
        create_dir(&e.certd_state())?;
        create_dir(&e.xdg_cache_home())?;
        create_dir(&e.scratch_state())?;

        let mut c = e.command("gpg");
        c.arg("--version");
        e.run(c);

        e.import(&e.willow.cert)?;
        e.import(&e.willow_release.cert)?;
        e.import(&e.buffy.cert)?;
        e.import(&e.xander.cert)?;
        e.import(&e.riley.cert)?;

        e.git(&["version"]);

        e.git(&["init", &e.git_state().display().to_string()])?;
        e.git(&["config", "--local", "user.email", "you@example.org"])?;
        e.git(&["config", "--local", "user.name", "Your Name"])?;

        // git's default is to not sign.  But, the user might have
        // overridden this in their ~/.gitconfig, and be using an old
        // version of git (<2.32).  In that case, GIT_CONFIG_GLOBAL
        // won't suppress this setting.  Setting it unconditionally in
        // the local configuration file is a sufficient workaround.
        e.git(&["config", "--local", "user.signingkey", "0xDEADBEEF"])?;
        e.git(&["config", "--local", "commit.gpgsign", "false"])?;

        Ok(e)
    }

    pub fn new() -> Result<Environment> {
        Self::at(None)
    }

    /// Persists the directory so that it can be examined after this run.
    #[allow(dead_code)]
    pub fn persist(&mut self) {
        self.wd.persist();
        eprintln!("Persisting temporary directory: {}",
                  self.wd.path().display());
    }

    /// Returns the environment and the root commit.
    #[allow(dead_code)]
    pub fn scooby_gang_bootstrap() -> Result<(Environment, String)> {
        let e = Environment::new()?;

        // Willow has a code-signing key.
        e.sq_git(&[
            "policy",
            "authorize",
            e.willow.petname,
            &e.willow.fingerprint.to_string(),
            "--sign-commit"
        ])?;

        // Additionally, Willow also has a release key on her security
        // token.
        e.sq_git(&[
            "policy",
            "authorize",
            e.willow_release.petname,
            &e.willow_release.fingerprint.to_string(),
            "--sign-commit",
            "--sign-tag",
            "--sign-archive",
            "--add-user",
            "--retire-user",
            "--audit",
        ])?;

        e.git(&["add", "openpgp-policy.toml"])?;
        e.git(&[
            "commit",
            "-m", "Initial commit.",
            &format!("-S{}", e.willow_release.fingerprint),
        ])?;
        let root = e.git_current_commit()?;
        Ok((e, root))
    }

    pub fn gnupg_state(&self) -> PathBuf {
        self.wd.path().join("gnupg")
    }

    pub fn git_state(&self) -> PathBuf {
        self.wd.path().join("git")
    }

    pub fn certd_state(&self) -> PathBuf {
        self.wd.path().join("certd")
    }

    pub fn xdg_cache_home(&self) -> PathBuf {
        self.wd.path().join("xdg_cache_home")
    }

    #[allow(dead_code)]
    pub fn scratch_state(&self) -> PathBuf {
        self.wd.path().join("scratch")
    }

    pub fn import(&self, cert: &Cert) -> Result<()> {
        let certd = CertD::open(self.certd_state())?;
        certd.update(Arc::new(cert.clone().into()))?;

        let mut c = self.command("gpg");
        c.arg("--status-fd=2");
        c.arg("--import").stdin(Stdio::piped());
        eprintln!("$ {:?} {}", c, cert.fingerprint());

        let mut child = self.spawn(c)?;

        // Write in a separate thread to avoid deadlocks.
        let mut stdin = child.stdin.take().expect("failed to get stdin");
        let cert = cert.clone();
        let thread_handle = std::thread::spawn(move || -> Result<()> {
            cert.as_tsk().serialize(&mut stdin)?;
            Ok(stdin.flush()?)
        });

        let output = child.wait_with_output()?;
        thread_handle.join().unwrap()?;

        if output.status.success() {
            eprintln!(" -> success");
            Ok(())
        } else {
            eprintln!(" -> failure");
            eprintln!("stdout:\n{}", String::from_utf8_lossy(&output.stdout));
            eprintln!("stderr:\n{}", String::from_utf8_lossy(&output.stderr));
            Err(Error::CliError("gpg --import".into(), output))
        }
    }

    /// Sets the time for subsequent commands.
    ///
    /// This causes commands to be run with `faketime`.
    ///
    /// Be sure to call `Self::check_for_faketime` to make sure using
    /// a fake time is supported.
    pub fn time(&mut self, t: SystemTime) {
        assert!(HAVE_FAKETIME.get().is_some(),
                "You forgot to call Environment::check_for_faketime \
                 before calling Environment::time");

        self.time = Some(t);
    }

    /// Advance the time.
    ///
    /// If you haven't called `time`, then this sets time to the
    /// current time plus `secs` seconds.
    ///
    /// Be sure to call `Self::check_for_faketime` to make sure using
    /// a fake time is supported.
    pub fn tick(&mut self, secs: u64) {
        assert!(HAVE_FAKETIME.get().is_some(),
                "You forgot to call Environment::check_for_faketime \
                 before calling Environment::tick");

        if let Some(t) = self.time {
            self.time = Some(t + Duration::new(secs, 0));
        } else {
            self.time = Some(SystemTime::now() + Duration::new(secs, 0));
        }
    }

    /// Returns a command.
    ///
    /// If `self.time` is `Some`, uses `faketime` to set the time.
    fn command<P>(&self, cmd: P) -> Command
    where P: Into<PathBuf>
    {
        let cmd = cmd.into();

        if let Some(t) = self.time.as_ref() {
            // Freeze clock at absolute timestamp: "YYYY-MM-DD hh:mm:ss"
            //
            // Note: we need to set the timezone to UTC.  This is
            // critical as faketime interprets the timestamp in the
            // current time zone.  We set TZ=UTC when calling spawn.

            let mut c = Command::new("faketime");
            c.env("TZ", "UTC");

            let t = chrono::DateTime::<chrono::Utc>::from(t.clone())
                .format("%Y-%m-%d %H:%M:%S")
                .to_string();
            c.arg("-f").arg(&t);

            c.arg(&cmd);
            c
        } else {
            Command::new(&cmd)
        }
    }

    pub fn git<A: AsRef<str>>(&self, args: &[A]) -> Result<Output> {
        let mut c = self.command("git");
        for a in args {
            c.arg(a.as_ref());
        }
        self.run(c)
    }

    // A convenience function to optionally modify and commit a few
    // files.
    //
    // Returns the new commit id.
    #[allow(dead_code)]
    pub fn git_commit(&self,
                      files: &[(&str, Option<&[u8]>)],
                      commit_msg: &str,
                      signer: Option<&Identity>)
        -> Result<String>
    {
        let p = self.git_state();

        for (filename, content) in files.iter() {
            if let Some(content) = content {
                std::fs::write(p.join(filename), content).unwrap();
            }
            self.git(&["add", filename])?;
        }

        let mut git_args = vec!["commit", "-m", commit_msg];
        let signer_;
        if let Some(signer) = signer {
            signer_ = format!("-S{}", signer.fingerprint);
            git_args.push(&signer_);
        }
        self.git(&git_args)?;

        Ok(self.git_current_commit()?)
    }

    pub fn git_current_commit(&self) -> Result<String> {
        Ok(String::from_utf8(self.git(&["rev-parse", "HEAD"])?.stdout)?
           .trim().to_string())
    }

    pub fn sq_git_path() -> Result<PathBuf> {
        use std::sync::Once;
        static BUILD: Once = Once::new();
        BUILD.call_once(|| {
            let o = Command::new("cargo")
                .arg("build").arg("--quiet")
                .arg("--bin").arg("sq-git")
                .output()
                .expect("running cargo failed");
            if ! o.status.success() {
                panic!("build failed:\n\nstdout:\n{}\n\n stderr:\n{}",
                        String::from_utf8_lossy(&o.stdout),
                        String::from_utf8_lossy(&o.stderr));
            }
        });

        Ok(if let Ok(target) = std::env::var("CARGO_TARGET_DIR") {
            PathBuf::from(target).canonicalize()?
        } else {
            std::env::current_dir()?.join("target")
        }.join("debug/sq-git"))
    }

    pub fn sq_git<A: AsRef<str>>(&self, args: &[A]) -> std::result::Result<Output, Error> {
        let mut c = self.command(Self::sq_git_path()?);

        // We are a machine, request machine-readable output.
        c.arg("--output-format=json");

        for a in args {
            c.arg(a.as_ref());
        }

        self.run(c)
    }

    pub fn spawn(&self, mut c: Command) -> Result<Child> {
        // Preserve any environment variables the caller explicitly
        // set (env_clear clears those and prevents the process from
        // inheriting any environment variables, but we only want the
        // latter).
        let env: Vec<_> = c.get_envs()
            .filter_map(|(k, v)| {
                if let Some(v) = v {
                    Some((k.to_os_string(), v.to_os_string()))
                } else {
                    None
                }
            })
            .collect::<Vec<(OsString, OsString)>>();

        let c = c.current_dir(self.git_state())
            .env_clear() // Filter out all git-related environment variables.
            .envs(std::env::vars()
                  .filter(|(k, _)| ! k.starts_with("GIT_"))
                  .collect::<Vec<_>>())
            .env("SEQUOIA_CERT_STORE", self.certd_state())
            .env("GNUPGHOME", self.gnupg_state())
            .env("GIT_CONFIG_GLOBAL", "/dev/null")
            .env("GIT_CONFIG_NOSYSTEM", "1")
            .env("XDG_CACHE_HOME", self.xdg_cache_home())
            .envs(env);

        Ok(c
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?)
    }

    pub fn run(&self, c: Command) -> Result<Output> {
        let cmd = format!("{:?}", c);

        eprintln!("$ {}", cmd);

        let output = self.spawn(c)?.wait_with_output()?;

        if output.status.success() {
            eprintln!(" -> success");
        } else {
            eprintln!(" -> failure");
        }
        eprintln!("stdout:\n{}", String::from_utf8_lossy(&output.stdout));
        eprintln!("stderr:\n{}", String::from_utf8_lossy(&output.stderr));

        if output.status.success() {
            Ok(output)
        } else {
            Err(Error::CliError(cmd, output))
        }
    }

    /// Serializes a certificate to a file.
    pub fn serialize_cert(&self, name: &str, cert: &Cert) -> String {
        let mut bytes = Vec::new();
        cert.as_tsk().serialize(&mut bytes)
            .expect("serializing to a vec is infallible");

        let cert_pgp = self.scratch_state().join(format!("{}.pgp", name));
        let mut f = File::create_new(&cert_pgp).expect("can create file");
        f.write_all(&bytes).expect("can write");
        drop(f);

        format!("{}", cert_pgp.display())
    }

    /// Generates a certificate with the user ID
    /// <localpart@example.org>.  Writes it into the scratch
    /// directory, and imports it into the certificate stores.
    ///
    /// Returns the certificate and the path to a file containing it.
    pub fn gen<C, V>(&self, localpart: &str, creation_time: C, validity: V)
        -> (Cert, String)
    where C: Into<Option<SystemTime>>,
          V: Into<Option<Duration>>,
    {
        // Be careful to create a certificate that won't be stripped on
        // import.
        let mut builder = CertBuilder::new()
            .add_userid(&format!("<{}@example.org>", localpart)[..])
            .add_signing_subkey();
        if let Some(t) = creation_time.into() {
            builder = builder.set_creation_time(t);
        }
        if let Some(v) = validity.into() {
            builder = builder.set_validity_period(v);
        }
        let (cert, _rev) = builder
            .generate()
            .expect("can generate a key");

        let filename = self.serialize_cert(localpart, &cert);

        self.import(&cert).expect("can import");

        (cert, filename)
    }

    /// Export the given entity's keyring, and make sure that it
    /// contains the specified certificates.  The certificates must
    /// match exactly.  Recall that adding a certificates prunes some
    /// components.
    ///
    /// It's okay if the entity does not exist.
    pub fn check_export<'a, E, C>(&self,
                                  entity: E,
                                  commit: C,
                                  expected: &[ &Cert ])
    where
        E: Into<Option<&'a str>>,
        C: Into<Option<&'a str>>,
    {
        let entity = entity.into();
        let commit = commit.into();

        let mut args = vec![ "policy", "export" ];
        if let Some(entity) = entity {
            args.push("--name");
            args.push(entity);
        } else {
            args.push("--all");
        }
        if let Some(commit) = commit {
            args.extend(&[ "--commit", commit]);
        }

        let got = if let Ok(output) = self.sq_git(&args[..]) {
            CertParser::from_bytes(&output.stdout)
                .expect("can parse keyring")
                .map(|r| r.map_err(Into::into))
                .collect::<Result<Vec<Cert>>>()
                .expect("can parse all certificates")
        } else {
            // entity does not exist.
            eprintln!("Warning: sq-git policy export failed: entity \
                       appears to not exist");
            Vec::new()
        };

        let got: BTreeMap<Fingerprint, Cert>
            = BTreeMap::from_iter(got.into_iter().map(|c| (c.fingerprint(), c)));
        let got_fprs = BTreeSet::from_iter(got.keys());

        let expected: BTreeMap<Fingerprint, Cert>
            = BTreeMap::from_iter(expected.iter().map(|&c| {
                (c.fingerprint(), c.clone().strip_secret_key_material())
            }));
        let expected_fprs = BTreeSet::from_iter(expected.keys());

        let mut die = false;

        for unexpected in got_fprs.difference(&expected_fprs) {
            let c = got.get(unexpected).unwrap();

            eprintln!("Unexpectedly got {} ({})",
                      unexpected,
                      c.userids().next().map(|ua| {
                          String::from_utf8_lossy(ua.userid().value())
                      })
                      .unwrap_or("<unknown>".into()));
            die = true;
        }
        for missing in expected_fprs.difference(&got_fprs) {
            let c = expected.get(missing).unwrap();

            eprintln!("Missing {} ({})",
                      missing,
                      c.userids().next().map(|ua| {
                          String::from_utf8_lossy(ua.userid().value())
                      })
                      .unwrap_or("<unknown>".into()));
            die = true;
        }
        for fpr in expected_fprs.intersection(&got_fprs) {
            let expected = expected.get(fpr).unwrap();
            let got = got.get(fpr).unwrap();

            if expected != got {
                // Is this failing unexpectedly?  Perhaps you created a
                // certificate with encryption or authentication capable
                // subkeys.  Recall: sq-git strips those on import.
                eprintln!("{} ({}) differs",
                          fpr,
                          expected.userids().next().map(|ua| {
                              String::from_utf8_lossy(ua.userid().value())
                          })
                          .unwrap_or("<unknown>".into()));

                eprintln!("Got ({} packets):",
                          got.clone().into_packets().count());
                let mut bytes = Vec::new();
                got.as_tsk().armored().serialize(&mut bytes)
                    .expect("serializing to a vec is infallible");
                eprintln!("{}", String::from_utf8_lossy(&bytes));

                eprintln!("Expected ({} packets):",
                          expected.clone().into_packets().count());
                let mut bytes = Vec::new();
                expected.as_tsk().armored().serialize(&mut bytes)
                    .expect("serializing to a vec is infallible");
                eprintln!("{}", String::from_utf8_lossy(&bytes));

                die = true;
            }
        }

        if die {
            panic!("{}'s certificates are unexpected",
                   entity.unwrap_or("--all"));
        }
    }
}

/// Errors for this crate.
#[derive(thiserror::Error, Debug)]
pub enum Error {

    #[error("command failed\n$ {}\nstdout:\n{}\n\nstderr:\n{}",
            .0,
            String::from_utf8_lossy(&.1.stdout),
            String::from_utf8_lossy(&.1.stderr))]
    CliError(String, std::process::Output),

    #[error("`faketime` not available (to skip tests that need `faketime`, \
             set the NO_FAKETIME environment variable)\n\
             \nstdout:\n{}\n\
             \nstderr:\n{}",
            String::from_utf8_lossy(&.0.stdout),
            String::from_utf8_lossy(&.0.stderr))]
    NoFaketime(std::process::Output),

    #[error(transparent)]
    Other(#[from] anyhow::Error),
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error(transparent)]
    Utf8(#[from] std::string::FromUtf8Error),
}

fn sh_quote<'s, S: AsRef<str> + 's>(s: S) -> String {
    let s = s.as_ref();
    if s.contains(char::is_whitespace) {
        format!("{:?}", s)
    } else {
        s.to_string()
    }
}
