//! A convenience type for using a
//! [SOP](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/)
//! implementation.

#![allow(unused_variables)]
#![allow(dead_code)]

use std::{
    ffi::OsString,
    fmt,
    path::{Path, PathBuf},
    process::Command,
};

use clingwrap::runner::{CommandError, CommandRunner};
use tempfile::tempdir;

use crate::util::{UtilError, read_file, write_file};

#[cfg(test)]
use crate::config::{DEFAULT_SOP, DEFAULT_SOP_CARD};

/// An OpenPGP key.
pub struct OpenPgpKey {
    key: Vec<u8>,
}

impl OpenPgpKey {
    /// Create a new key from contents of a key file.
    pub fn new(key: &[u8]) -> Self {
        Self { key: key.to_vec() }
    }

    /// Read a key from a file.
    pub fn read<P: AsRef<Path>>(filename: P) -> Result<Self, SopError> {
        let filename = filename.as_ref();
        let key =
            read_file(filename).map_err(|err| SopError::ReadKey(filename.to_path_buf(), err))?;
        let key = OpenPgpKey::new(&key);
        Ok(key)
    }

    /// Return serialized key as bytes.
    pub fn as_bytes(&self) -> &[u8] {
        &self.key
    }
}

impl fmt::Display for OpenPgpKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", String::from_utf8_lossy(&self.key))
    }
}

/// An OpenPGP certificate.
#[derive(Clone)]
pub struct OpenPgpCert {
    cert: Vec<u8>,
}

impl OpenPgpCert {
    /// Create a new certificate from contents of a certificate file.
    pub fn new(cert: &[u8]) -> Self {
        Self {
            cert: cert.to_vec(),
        }
    }

    /// Read a certificate from a file.
    pub fn read<P: AsRef<Path>>(filename: P) -> Result<Self, SopError> {
        let filename = filename.as_ref();
        let key =
            read_file(filename).map_err(|err| SopError::ReadKey(filename.to_path_buf(), err))?;
        let key = OpenPgpCert::new(&key);
        Ok(key)
    }

    fn as_bytes(&self) -> &[u8] {
        &self.cert
    }
}

impl From<&[u8]> for OpenPgpCert {
    fn from(cert: &[u8]) -> Self {
        Self::new(cert)
    }
}

impl fmt::Display for OpenPgpCert {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", String::from_utf8_lossy(&self.cert))
    }
}

/// Execute a SOP implementation for software keys and a similar tool
/// for using OpenPGP cards, such as Heiko Schaefer's `rsoct`.
pub struct SopCard {
    cert_command: PathBuf,
    key_command: PathBuf,
}

impl SopCard {
    /// Create a `Sop` for using a given implementation.
    pub fn new(cert_command: &Path, key_command: &Path) -> Self {
        Self {
            cert_command: cert_command.into(),
            key_command: key_command.into(),
        }
    }

    /// Generate a new OpenPGP key.
    pub fn generate_key(&self, userid: &str) -> Result<OpenPgpKey, SopError> {
        let mut cmd = Command::new(&self.cert_command);
        cmd.arg("generate-key");
        cmd.arg(userid);

        let mut runner = CommandRunner::new(cmd);
        runner.capture_stdout();
        runner.capture_stderr();

        let output = runner.execute().map_err(SopError::GenerateKey)?;

        let key = OpenPgpKey::new(output.stdout.as_slice());
        Ok(key)
    }

    /// Extract a certificate from a software key.
    pub fn extract_cert(&self, key: &OpenPgpKey) -> Result<OpenPgpCert, SopError> {
        let mut cmd = Command::new(&self.cert_command);
        cmd.arg("extract-cert");

        let mut runner = CommandRunner::new(cmd);
        runner.feed_stdin(key.as_bytes());
        runner.capture_stdout();
        runner.capture_stderr();

        let output = runner.execute().map_err(SopError::ExtractCert)?;

        let cert = OpenPgpCert::from(output.stdout.as_slice());
        Ok(cert)
    }

    /// Encrypt data using certificates.
    #[mutants::skip] // we can't test this without suitable hardware
    pub fn encrypt(&self, certs: &[OpenPgpCert], data: &[u8]) -> Result<Vec<u8>, SopError> {
        let mut cmd = Command::new(&self.cert_command);
        cmd.arg("encrypt");

        let tmp = tempdir().map_err(SopError::TempDir)?;
        for (i, cert) in certs.iter().enumerate() {
            let filename = tmp.path().join(format!("{i}.cert"));
            write_cert(&filename, cert)?;
            cmd.arg(&filename);
        }

        let mut runner = CommandRunner::new(cmd);
        runner.feed_stdin(data);
        runner.capture_stdout();
        runner.capture_stderr();
        let output = runner.execute().map_err(SopError::Encrypt)?;
        Ok(output.stdout)
    }

    /// Decrypt data using key on an OpenPGP card.
    #[mutants::skip] // we can't test this without suitable hardware
    pub fn decrypt(&self, cert: &OpenPgpCert, ciphertext: &[u8]) -> Result<Vec<u8>, SopError> {
        let mut cmd = Command::new(&self.key_command);
        cmd.arg("decrypt");

        let tmp = tempdir().map_err(SopError::TempDir)?;
        let filename = tmp.path().join("cert");
        write_cert(&filename, cert)?;
        cmd.arg(&filename);

        let mut runner = CommandRunner::new(cmd);
        runner.feed_stdin(ciphertext);
        runner.capture_stdout();
        runner.capture_stderr();
        let output = runner.execute().map_err(SopError::Decrypt)?;
        Ok(output.stdout)
    }
}

#[cfg(test)]
impl Default for SopCard {
    fn default() -> Self {
        Self::new(Path::new(DEFAULT_SOP), Path::new(DEFAULT_SOP_CARD))
    }
}

/// Execute a SOP implementation using software keys only.
pub struct Sop {
    command: OsString,
}

impl Sop {
    /// Create a `Sop` for using a given implementation.
    pub fn new(command: &Path) -> Self {
        Self {
            command: command.into(),
        }
    }

    /// Generate a new OpenPGP key.
    pub fn generate_key(&self, userid: &str) -> Result<OpenPgpKey, SopError> {
        let mut cmd = Command::new(&self.command);
        cmd.arg("generate-key");
        cmd.arg(userid);

        let mut runner = CommandRunner::new(cmd);
        runner.capture_stdout();
        runner.capture_stderr();

        let output = runner.execute().map_err(SopError::GenerateKey)?;

        let key = OpenPgpKey::new(output.stdout.as_slice());
        Ok(key)
    }

    /// Extract a certificate from a key.
    pub fn extract_cert(&self, key: &OpenPgpKey) -> Result<OpenPgpCert, SopError> {
        let mut cmd = Command::new(&self.command);
        cmd.arg("extract-cert");

        let mut runner = CommandRunner::new(cmd);
        runner.feed_stdin(key.as_bytes());
        runner.capture_stdout();
        runner.capture_stderr();

        let output = runner.execute().map_err(SopError::ExtractCert)?;

        let cert = OpenPgpCert::from(output.stdout.as_slice());
        Ok(cert)
    }

    /// Encrypt data using certificates.
    pub fn encrypt(&self, certs: &[OpenPgpCert], data: &[u8]) -> Result<Vec<u8>, SopError> {
        let mut cmd = Command::new(&self.command);
        cmd.arg("encrypt");

        let tmp = tempdir().map_err(SopError::TempDir)?;
        for (i, cert) in certs.iter().enumerate() {
            let filename = tmp.path().join(format!("{i}.cert"));
            write_cert(&filename, cert)?;
            cmd.arg(&filename);
        }

        let mut runner = CommandRunner::new(cmd);
        runner.feed_stdin(data);
        runner.capture_stdout();
        runner.capture_stderr();
        let output = runner.execute().map_err(SopError::Encrypt)?;
        Ok(output.stdout)
    }

    /// Decrypt data using key.
    pub fn decrypt(&self, key: &OpenPgpKey, ciphertext: &[u8]) -> Result<Vec<u8>, SopError> {
        let mut cmd = Command::new(&self.command);
        cmd.arg("decrypt");

        let tmp = tempdir().map_err(SopError::TempDir)?;
        let filename = tmp.path().join("key");
        write_key(&filename, key)?;
        cmd.arg(&filename);

        let mut runner = CommandRunner::new(cmd);
        runner.feed_stdin(ciphertext);
        runner.capture_stdout();
        runner.capture_stderr();
        let output = runner.execute().map_err(SopError::Decrypt)?;
        Ok(output.stdout)
    }
}

#[cfg(test)]
impl Default for Sop {
    fn default() -> Self {
        Self::new(Path::new(DEFAULT_SOP))
    }
}

/// Read an [`OpenPgpKey`] from a file.
pub fn read_key(filename: &Path) -> Result<OpenPgpKey, SopError> {
    let key = read_file(filename).map_err(|err| SopError::ReadKey(filename.to_path_buf(), err))?;
    let key = OpenPgpKey::new(&key);
    Ok(key)
}

/// Read an [`OpenPgpCert`] from a file.
pub fn read_cert(filename: &Path) -> Result<OpenPgpCert, SopError> {
    let cert = read_file(filename).map_err(|err| SopError::ReadKey(filename.to_path_buf(), err))?;
    let cert = OpenPgpCert::new(&cert);
    Ok(cert)
}

/// Write a certificate to a file.
pub fn write_cert(filename: &Path, cert: &OpenPgpCert) -> Result<(), SopError> {
    write_file(filename, cert.as_bytes()).map_err(SopError::CertFile)?;
    Ok(())
}

/// Write a key to a file.
pub fn write_key(filename: &Path, key: &OpenPgpKey) -> Result<(), SopError> {
    write_file(filename, key.as_bytes()).map_err(SopError::KeyFile)?;
    Ok(())
}

/// Possible errors from using [`Sop`].
#[derive(Debug, thiserror::Error)]
pub enum SopError {
    /// Can't read key from file.
    #[error("failed to read key file {0}")]
    ReadKey(PathBuf, #[source] UtilError),

    /// Can't generate a key.
    #[error("failed to generate a key")]
    GenerateKey(#[source] CommandError),

    /// Can't extract cert from key.
    #[error("failed to extract cert from key")]
    ExtractCert(#[source] CommandError),

    /// Can't encrypt.
    #[error("failed to encrypt with SOP")]
    Encrypt(#[source] CommandError),

    /// Can't decrypt.
    #[error("failed to decrypt with SOP")]
    Decrypt(#[source] CommandError),

    /// Can't create temporary directory.
    #[error("failed to create temporary directory")]
    TempDir(#[source] std::io::Error),

    /// Can't write cert to temporary file.
    #[error("failed to write an OpenPGP certificate to a file")]
    CertFile(#[source] UtilError),

    /// Can't write key to temporary file.
    #[error("failed to write an OpenPGP key to a file")]
    KeyFile(#[source] UtilError),
}

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

    #[test]
    fn cert_display() {
        let cert = OpenPgpCert::new(b"xyzzy");
        assert_eq!(cert.to_string(), "xyzzy");
    }

    #[test]
    fn key_display() {
        let key = OpenPgpKey::new(b"xyzzy");
        assert_eq!(key.to_string(), "xyzzy");
    }
}
