use std::{
    fs::File,
    io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Stdin, Stdout, Write},
    path::{Path, PathBuf},
};

pub mod rsa;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("{0}")]
    Io(#[from] io::Error),
    #[error("RSA error: {0}")]
    Rsa(#[from] ::rsa::Error),
    #[error("PKCS#1 error: {0}")]
    Pkcs1(#[from] ::rsa::pkcs1::Error),
    #[error("PKCS#8 error: {0}")]
    Pkcs8(#[from] ::rsa::pkcs8::Error),
    #[error("PKCS#8 error: {0}")]
    Pkcs8Spki(#[from] ::rsa::pkcs8::spki::Error),
    #[error("incompatible options: {0}")]
    IncompatibleOptions(String),
}

#[derive(Debug, clap::Parser)]
pub struct AsymmetricEncryptOptions {
    #[clap(short, long, help = "Ciphertext output (default: stdout)")]
    pub output: Option<PathBuf>,
    #[clap(short = 'k', long, help = "Public key to use")]
    pub public_key: PathBuf,
    #[clap(help = "Plaintext input (default: stdin)")]
    pub input: Option<PathBuf>,
}

#[derive(Debug, clap::Parser)]
pub struct AsymmetricDecryptOptions {
    #[clap(short, long, help = "Plaintext output (default: stdout)")]
    pub output: Option<PathBuf>,
    #[clap(short = 'k', long, help = "Private key to use")]
    pub private_key: PathBuf,
    #[clap(help = "Ciphertext input (default: stdin)")]
    pub input: Option<PathBuf>,
}

#[derive(Debug, clap::Parser)]
pub struct AsymmetricKeyOptions {
    #[clap(short = 'o', long, help = "Public key output file (default: stdout)")]
    pub public_key: Option<PathBuf>,
    #[clap(help = "Private key output file")]
    pub private_key: PathBuf,
}

#[derive(Debug)]
pub enum Input {
    Stdin(Stdin),
    File(BufReader<File>),
}

#[derive(Debug)]
pub enum Output {
    Stdout(Stdout),
    File(BufWriter<File>),
}

impl Input {
    pub fn from_path_option<P: AsRef<Path>>(option: Option<P>) -> Result<Self, io::Error> {
        match option {
            Some(path) => File::open(path).map(BufReader::new).map(Self::File),
            None => Ok(Self::Stdin(stdin())),
        }
    }

    pub fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
        match self {
            Self::Stdin(stdin) => stdin.read_line(buf),
            Self::File(file) => file.read_line(buf),
        }
    }
}

impl Read for Input {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        match self {
            Self::Stdin(stdin) => stdin.read(buf),
            Self::File(file) => file.read(buf),
        }
    }
}

impl Output {
    pub fn from_path_option<P: AsRef<Path>>(option: Option<P>) -> Result<Self, io::Error> {
        match option {
            Some(path) => File::create(path).map(BufWriter::new).map(Self::File),
            None => Ok(Self::Stdout(stdout())),
        }
    }
}

impl Write for Output {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        match self {
            Self::Stdout(stdout) => stdout.write(buf),
            Self::File(file) => file.write(buf),
        }
    }

    fn flush(&mut self) -> io::Result<()> {
        match self {
            Self::Stdout(stdout) => stdout.flush(),
            Self::File(file) => file.flush(),
        }
    }
}
