use std::{
    process::{Child, Command, Output, Stdio},
    path::Path,
    path::PathBuf,
    time::{Duration, SystemTime},
};

use anyhow::{anyhow, Result};

use sequoia_openpgp::{
    Fingerprint,
    cert::{
        Cert,
        CertBuilder,
    },
    packet::Signature,
    serialize::Serialize,
};
use sequoia_cert_store::{
    StoreUpdate,
    store::certd::CertD,
};

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(None, 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")
    }
}

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 {
    pub wd: TempDir,
    pub willow: Identity,
    pub willow_release: Identity,
    pub buffy: Identity,
    pub xander: Identity,
    pub riley: Identity,
}

impl Environment {
    pub fn new() -> Result<Environment> {
        let e = Environment {
            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")?,
        };

        std::fs::create_dir(e.gnupg_state())?;
        std::fs::create_dir(e.git_state())?;
        std::fs::create_dir(e.certd_state())?;
        std::fs::create_dir(e.xdg_cache_home())?;
        std::fs::create_dir(e.scratch_state())?;

        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(&["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)
    }

    /// 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 mut certd = CertD::open(self.certd_state())?;
        certd.update(std::borrow::Cow::Owned(cert.clone().into()))?;

        let mut c = Command::new("gpg");
        c.arg("--status-fd=2");
        c.arg("--import").stdin(Stdio::piped());

        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 || {
            cert.as_tsk().serialize(&mut stdin)
        });

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

        if output.status.success() {
            Ok(())
        } else {
            Err(anyhow!("gpg --import failed\n\nstdout:\n{}\n\n stderr:\n{}",
                        String::from_utf8_lossy(&output.stdout),
                        String::from_utf8_lossy(&output.stderr)))
        }
    }

    pub fn git<A: AsRef<str>>(&self, args: &[A]) -> Result<(Vec<u8>, Vec<u8>)> {
        eprint!("$ git");
        let mut c = Command::new("git");
        for a in args {
            eprint!(" {}", a.as_ref());
            c.arg(a.as_ref());
        }
        eprintln!();
        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"])?.0)?
           .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]) -> Result<Output> {
        eprint!("$ sq-git");

        let mut c = Command::new(Self::sq_git_path()?);

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

        for a in args {
            eprint!(" {}", sh_quote(a));
            c.arg(a.as_ref());
        }
        eprintln!();

        let output = self.spawn(c)?.wait_with_output()?;
        if output.status.success() {
            Ok(output)
        } else {
            Err(CliError { output }.into())
        }
    }

    pub fn spawn(&self, mut c: Command) -> Result<Child> {
        Ok(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("SQ_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())
           .stdout(Stdio::piped())
           .stderr(Stdio::piped())
           .spawn()?)
    }

    pub fn run(&self, c: Command) -> Result<(Vec<u8>, Vec<u8>)> {
        let output = self.spawn(c)?.wait_with_output()?;
        if output.status.success() {
            Ok((output.stdout, output.stderr))
        } else {
            Err(anyhow!("command failed\n\nstdout:\n{}\n\nstderr:\n{}",
                        String::from_utf8_lossy(&output.stdout),
                        String::from_utf8_lossy(&output.stderr)))
        }
    }
}

/// Errors for this crate.
#[derive(thiserror::Error, Debug)]
#[error("command failed\n\nstdout:\n{}\n\nstderr:\n{}",
        String::from_utf8_lossy(&self.output.stdout),
        String::from_utf8_lossy(&self.output.stderr))]
pub struct CliError {
    output: std::process::Output,
}

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()
    }
}
