use std::path::PathBuf;

use clap::Parser;

use log::debug;
use obnam::{
    config::Config,
    sop::{OpenPgpCert, OpenPgpKey, Sop, SopCard, SopError},
    util::{UtilError, lossy_string, read_file, write, write_stdout},
};

use super::{Args, Leaf};

/// Extract an OpenPGP certificate from an OpenPGP key.
#[derive(Parser)]
pub struct ExtractCert {
    /// Name of SOP implementation to use.
    sop: PathBuf,

    /// Name of file where key is.
    key: PathBuf,
}

impl ExtractCert {
    fn sop_name(&self) -> String {
        lossy_string(&self.sop)
    }
}

impl Leaf for ExtractCert {
    type Error = SopCmdError;

    fn run(&self, _args: &Args, _config: &Config) -> Result<(), Self::Error> {
        debug!(
            "Extract certificate from OpenPGP key with SOP implementation {}",
            self.sop_name()
        );
        let key = OpenPgpKey::read(&self.key)
            .map_err(|err| SopCmdError::ReadKey(self.key.clone(), err))?;
        let sop = Sop::new(&self.sop);
        let cert = sop.extract_cert(&key).map_err(SopCmdError::ExtractCert)?;
        print!("{cert}");
        Ok(())
    }
}

/// Encrypt data in a file with a certificate extracted from a key in a file.
#[derive(Debug, Parser)]
pub struct Encrypt {
    /// Name of SOP implementation to use.
    sop: PathBuf,

    /// Name of file where key is.
    key: PathBuf,

    /// File where plain text data is.
    filename: PathBuf,

    /// Write output to this file instead of the standard output.
    #[clap(short, long)]
    output: Option<PathBuf>,
}

impl Encrypt {
    fn sop_name(&self) -> String {
        lossy_string(&self.sop)
    }
}

impl Leaf for Encrypt {
    type Error = SopCmdError;

    fn run(&self, _args: &Args, _config: &Config) -> Result<(), Self::Error> {
        debug!(
            "Encrypt data using OpenPGP key with SOP implementation {}",
            self.sop_name()
        );
        let key = OpenPgpKey::read(&self.key)
            .map_err(|err| SopCmdError::ReadKey(self.key.clone(), err))?;
        let sop = Sop::new(&self.sop);
        let cert = sop.extract_cert(&key).map_err(SopCmdError::ExtractCert)?;

        let data = read_file(&self.filename)
            .map_err(|err| SopCmdError::ReadFile(self.filename.clone(), err))?;
        let encrypted = sop.encrypt(&[cert], &data).map_err(SopCmdError::Encrypt)?;

        write(&self.output, &encrypted).map_err(SopCmdError::Write)?;

        Ok(())
    }
}

/// Decrypt data in a file with a key in a file.
#[derive(Parser)]
pub struct Decrypt {
    /// Name of SOP implementation to use.
    sop: PathBuf,

    /// Name of SOP-like program that supports OpenPGP card to use.
    /// For example, `rsoct`.
    #[clap(long)]
    sop_card: Option<PathBuf>,

    /// Name of file where key is. If `--sop-card` is used, the file should
    /// contain a certificate instead.
    key: PathBuf,

    /// File where ciphertext data is.
    filename: PathBuf,
}

impl Decrypt {
    fn sop_name(&self) -> String {
        lossy_string(&self.sop)
    }
}

impl Leaf for Decrypt {
    type Error = SopCmdError;

    fn run(&self, _args: &Args, _config: &Config) -> Result<(), Self::Error> {
        debug!(
            "Decrypt data using OpenPGP key with SOP implementation {}",
            self.sop_name()
        );
        if let Some(sop_card) = &self.sop_card {
            let cert = OpenPgpCert::read(&self.key)
                .map_err(|err| SopCmdError::ReadCert(self.key.clone(), err))?;

            let ciphertext = read_file(&self.filename)
                .map_err(|err| SopCmdError::ReadFile(self.filename.clone(), err))?;

            let sop = SopCard::new(&self.sop, sop_card);
            let data = sop
                .decrypt(&cert, &ciphertext)
                .map_err(SopCmdError::Decrypt)?;
            write_stdout(&data).map_err(SopCmdError::Write)?;
        } else {
            let key = OpenPgpKey::read(&self.key)
                .map_err(|err| SopCmdError::ReadKey(self.key.clone(), err))?;

            let ciphertext = read_file(&self.filename)
                .map_err(|err| SopCmdError::ReadFile(self.filename.clone(), err))?;

            let sop = Sop::new(&self.sop);
            let data = sop
                .decrypt(&key, &ciphertext)
                .map_err(SopCmdError::Decrypt)?;
            write_stdout(&data).map_err(SopCmdError::Write)?;
        }

        Ok(())
    }
}

#[derive(Debug, thiserror::Error)]
pub enum SopCmdError {
    #[error("failed to read key from file")]
    ReadKey(PathBuf, #[source] obnam::sop::SopError),

    #[error("failed to read certificate from file")]
    ReadCert(PathBuf, #[source] obnam::sop::SopError),

    #[error("failed to extract certificate from key")]
    ExtractCert(#[source] SopError),

    #[error("failed to encrypt with certificate")]
    Encrypt(#[source] SopError),

    #[error("failed to decrypt with key")]
    Decrypt(#[source] SopError),

    #[error("failed to read data from file {0}")]
    ReadFile(PathBuf, #[source] UtilError),

    #[error("failed to write data")]
    Write(#[source] UtilError),
}
