//! Use the specified SOP implementation.
//!
//! This module makes it convenient to use the SOP implementation that
//! is meant to be used by `sopass`.

use std::{
    ffi::OsStr,
    fs::File,
    os::unix::ffi::OsStrExt,
    path::{Path, PathBuf},
    process::{Command, Stdio},
};

use clingwrap::runner::CommandRunner;
use log::info;
use tempfile::{tempdir, NamedTempFile};

/// The SOP implementation and associated private key file to use.
pub struct Sop {
    kind: SopKind,
}

impl Sop {
    /// Create a new [`Sop`] using a kind that uses OpenPGP cards.
    pub fn hardware_key(sop: &Path, sop_decrypt: &Path, cert: &Path) -> Self {
        Self {
            kind: SopKind::HardwareKey(HardwareKeySop::new(sop, sop_decrypt, cert)),
        }
    }

    /// Create a new [`Sop`] using a kind that uses software keys only.
    pub fn software_key(sop: &Path, key: &Path) -> Self {
        Self {
            kind: SopKind::SoftwareKey(SoftwareKeySop::new(sop, key)),
        }
    }

    /// Extract a [`Certificate`] from the private key.
    pub fn extract_cert(&self) -> Result<Certificate, SopError> {
        match &self.kind {
            SopKind::HardwareKey(sop) => sop.extract_cert(),
            SopKind::SoftwareKey(sop) => sop.extract_cert(),
        }
    }

    /// Encrypt data using a set of certificates.
    pub fn encrypt(
        &self,
        data: Vec<u8>,
        certs: &[Certificate],
        output: &Path,
    ) -> Result<Vec<u8>, SopError> {
        match &self.kind {
            SopKind::HardwareKey(sop) => sop.encrypt(data, certs, output),
            SopKind::SoftwareKey(sop) => sop.encrypt(data, certs, output),
        }
    }

    /// Decrypt data using the private key specified.
    pub fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, SopError> {
        match &self.kind {
            SopKind::HardwareKey(sop) => sop.decrypt(data),
            SopKind::SoftwareKey(sop) => sop.decrypt(data),
        }
    }
}

enum SopKind {
    HardwareKey(HardwareKeySop),
    SoftwareKey(SoftwareKeySop),
}

struct SoftwareKeySop {
    sop: PathBuf,
    key: PathBuf,
}

impl SoftwareKeySop {
    /// Create a new [`Sop`].
    pub fn new(sop: &Path, key: &Path) -> Self {
        Self {
            sop: sop.into(),
            key: key.into(),
        }
    }

    /// Extract a [`Certificate`] from the private key.
    pub fn extract_cert(&self) -> Result<Certificate, SopError> {
        let output = run_sop(
            &self.sop,
            &args(&["extract-cert"]),
            self.key()?.as_bytes().to_vec(),
            None,
        )?;
        Ok(Certificate::new(output))
    }

    /// Encrypt data using a set of certificates.
    pub fn encrypt(
        &self,
        data: Vec<u8>,
        certs: &[Certificate],
        output_file: &Path,
    ) -> Result<Vec<u8>, SopError> {
        encrypt_with_sop(&self.sop, data, certs, output_file)
    }

    /// Decrypt data using the private key specified.
    pub fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, SopError> {
        let mut args = args(&["decrypt"]);
        args.push(self.key.as_os_str());
        run_sop(&self.sop, &args, data, None)
    }

    fn key(&self) -> Result<Key, SopError> {
        let data =
            std::fs::read(&self.key).map_err(|err| SopError::ReadKey(self.key.clone(), err))?;
        Ok(Key::new(data))
    }
}

#[allow(dead_code)]
struct HardwareKeySop {
    sop: PathBuf,
    sop_decrypt: PathBuf,
    cert: PathBuf,
}

#[allow(unused_variables)]
impl HardwareKeySop {
    /// Create a new [`Sop`].
    pub fn new(sop: &Path, sop_decrypt: &Path, cert: &Path) -> Self {
        Self {
            sop: sop.into(),
            sop_decrypt: sop_decrypt.into(),
            cert: cert.into(),
        }
    }

    /// Extract a [`Certificate`] from the private key.
    pub fn extract_cert(&self) -> Result<Certificate, SopError> {
        info!("read certificate from {}", self.cert.display());
        let cert =
            std::fs::read(&self.cert).map_err(|err| SopError::ReadCert(self.cert.clone(), err))?;
        Ok(Certificate::new(cert))
    }

    /// Encrypt data using a set of certificates.
    pub fn encrypt(
        &self,
        data: Vec<u8>,
        certs: &[Certificate],
        output_file: &Path,
    ) -> Result<Vec<u8>, SopError> {
        encrypt_with_sop(&self.sop, data, certs, output_file)
    }

    /// Decrypt data using the private key specified.
    pub fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, SopError> {
        let mut args = args(&["decrypt"]);
        args.push(self.cert.as_os_str());
        run_sop(&self.sop_decrypt, &args, data, None)
    }
}

fn args<'a>(strs: &'a [&str]) -> Vec<&'a OsStr> {
    strs.iter()
        .map(|s| OsStr::from_bytes(s.as_bytes()))
        .collect()
}

fn encrypt_with_sop(
    bin: &Path,
    data: Vec<u8>,
    certs: &[Certificate],
    output_file: &Path,
) -> Result<Vec<u8>, SopError> {
    info!(
        "encrypt data to {} using certificates and {}",
        output_file.display(),
        bin.display()
    );

    let tmp = tempdir().map_err(SopError::TempDir)?;

    let mut filenames = vec![];
    for (i, cert) in certs.iter().enumerate() {
        let filename = format!("cert-{i}");
        let filename = tmp.path().join(&filename);
        std::fs::write(&filename, cert.as_bytes()).map_err(SopError::TempWrite)?;
        filenames.push(PathBuf::from(&filename));
    }

    let mut args = args(&["encrypt"]);
    for filename in filenames.iter() {
        args.push(filename.as_os_str());
    }

    let dir = output_file
        .parent()
        .ok_or_else(|| SopError::NoParent(output_file.to_path_buf()))?;

    let new_file = NamedTempFile::new_in(dir)
        .map_err(|err| SopError::CreateNamedTemp(dir.to_path_buf(), err))?;
    let stdout = File::create(new_file.path())
        .map_err(|err| SopError::CreateFile(new_file.path().to_path_buf(), err))?;
    let output = run_sop(bin, &args, data, Some(stdout))?;
    new_file
        .persist(output_file)
        .map_err(|err| SopError::Persist(output_file.to_path_buf(), err))?;

    Ok(output)
}

fn run_sop(
    bin: &Path,
    args: &[&OsStr],
    feed_stdin: Vec<u8>,
    stdout: Option<File>,
) -> Result<Vec<u8>, SopError> {
    let mut cmd = Command::new(bin);
    cmd.args(args);

    let capture_stdout = if let Some(file) = stdout {
        cmd.stdout(Stdio::from(file));
        false
    } else {
        true
    };

    let mut runner = CommandRunner::new(cmd);
    runner.feed_stdin(&feed_stdin);
    if capture_stdout {
        runner.capture_stdout();
    }
    runner.capture_stderr();

    let output = runner
        .execute()
        .map_err(|source| SopError::sop_failed(bin, source))?;

    Ok(output.stdout)
}

/// A private key in memory.
pub struct Key {
    data: Vec<u8>,
}

impl Key {
    /// Create a [`Key`] from key materiel.
    pub fn new(data: Vec<u8>) -> Self {
        Self { data }
    }

    /// The key materiel as a byte slice.
    pub fn as_bytes(&self) -> &[u8] {
        &self.data
    }
}

/// A certificate in memory.
#[derive(Debug)]
pub struct Certificate {
    data: Vec<u8>,
}

impl Certificate {
    /// Create a [`Certificate`] from bytes.
    pub fn new(data: Vec<u8>) -> Self {
        Self { data }
    }

    /// Load a certificate from a file.
    pub fn load(filename: &Path) -> Result<Self, SopError> {
        let bytes =
            std::fs::read(filename).map_err(|err| SopError::ReadCert(filename.into(), err))?;
        Ok(Self::new(bytes))
    }

    /// Access a [`Certificate`] as a byte slice.
    pub fn as_bytes(&self) -> &[u8] {
        &self.data
    }
}

/// Possible errors from using [`Sop`].
#[derive(Debug, thiserror::Error)]
pub enum SopError {
    /// Failed to invoke the SOP implementation, for example, if the
    /// executable doesn't exist.
    #[error("failed to run {0}")]
    Invoke(PathBuf, #[source] std::io::Error),

    /// The SOP implementation failed to do what was requested.
    #[error("{0} failed with exit code {1}, stderr:\n{2}")]
    Failed(PathBuf, i32, String),

    /// Failed to redirect stdin of SOP implementation.
    #[error("failed to get stdin from child process handle")]
    TakeStdin,

    /// Failed to read the private key file.
    #[error("failed to read key from {0}")]
    ReadKey(PathBuf, #[source] std::io::Error),

    /// Failed to join thread.
    #[error("failed to join thread that writes to child stdin")]
    JoinThread,

    /// Failed to write to SOP implementation stdin.
    #[error("failed to write key to child stdin")]
    WriteStdin(#[source] std::io::Error),

    /// Failed to wait for SOP implementation to finish.
    #[error("failed when waiting for child process to end")]
    WaitChild(#[source] std::io::Error),

    /// Failed to create file to run SOP.
    #[error("failed to create file {0}")]
    CreateFile(PathBuf, #[source] std::io::Error),

    /// Failed to write captured standard output to file.
    #[error("failed to write captured output to file")]
    WriteStdout(#[source] std::io::Error),

    /// Failed to create directory to run SOP.
    #[error("failed to create a temporary directory")]
    TempDir(#[source] std::io::Error),

    /// Failed to write to temporary file to run SOP.
    #[error("failed to write to temporary file")]
    TempWrite(#[source] std::io::Error),

    /// Failed to open temporary file for reading.
    #[error("failed to open temporary file")]
    TempRead(#[source] std::io::Error),

    /// Failed to read certificate from a file.
    #[error("failed to read certificate from file {0}")]
    ReadCert(PathBuf, #[source] std::io::Error),

    /// Error running SOP implementation.
    #[error("failed to run SOP implementation {sop}")]
    SopFailed {
        /// Name of SOP implementation.
        sop: PathBuf,

        /// Underlying error.
        source: clingwrap::runner::CommandError,
    },

    /// Can't get parent directory of SOP output file.
    #[error("failed to get parent directory of {0}")]
    NoParent(PathBuf),

    /// Can't create temporary file in a directory.
    #[error("failed to create temporary file in directory {0}")]
    CreateNamedTemp(PathBuf, #[source] std::io::Error),

    /// Can't persiste temporary file.
    #[error("failed to save temporary file as actual file {0}")]
    Persist(PathBuf, #[source] tempfile::PersistError),
}

impl SopError {
    fn sop_failed(bin: &Path, source: clingwrap::runner::CommandError) -> Self {
        Self::SopFailed {
            sop: bin.to_path_buf(),
            source,
        }
    }
}
