//! Command-line frontend for SOP.
//!
//! This is an implementation of the SOP Command-Line protocol in
//! terms of the trait [`crate::SOP`], hence it can be shared between
//! SOP implementations.
//!
//! To use, add this snippet to `Cargo.toml`:
//!
//! ```toml
//! [[bin]]
//! path = "src/main.rs"
//! required-features = ["cli"]
//!
//! [features]
//! cli = ["sop/cli"]
//! ```
//!
//! And create `src/main.rs` along the lines of:
//!
//! ```rust,ignore
//! fn main() {
//!     sop::cli::main(&MySOPImplementation::default());
//! }
//! ```
//!
//! ## Generating shell completions
//!
//! To create shell completions, add this snippet to `Cargo.toml`:
//!
//! ```toml
//! [build-dependencies]
//! sop = "..."
//! ```
//!
//! And create `build.rs` along the lines of:
//!
//! ```rust,no_run
//! #[cfg(feature = "cli")]
//! fn main() {
//!     let outdir = std::env::var_os("CARGO_TARGET_DIR")
//!         .or(std::env::var_os("OUT_DIR"))
//!         .expect("cargo to set OUT_DIR");
//!     sop::cli::write_shell_completions("sqop", outdir).unwrap();
//! }
//!
//! #[cfg(not(feature = "cli"))]
//! fn main() {}
//! ```
//!
//! # Features and limitations
//!
//! - The special designator `@FD:` is only available on UNIX-like systems.
//!
//! - On Windows, certs and keys provided via the `@ENV:` special
//!   designator must be ASCII armored and well-formed UTF-8.

use std::{
    io::{self, Write},
    path::Path,
};

use anyhow::{Context, Result};

use chrono::{DateTime, offset::Utc};
use structopt::{StructOpt, clap::AppSettings};

use crate::{
    ArmorLabel,
    EncryptAs,
    SignAs,
};

#[derive(Debug, StructOpt)]
#[structopt(about = "An implementation of the \
                     Stateless OpenPGP Command Line Interface",
            settings(&[AppSettings::DisableVersion,
                       AppSettings::VersionlessSubcommands]),
            )]
enum Operation {
    /// Prints version information.
    ///
    /// Invoked without arguments, returns name and version of the SOP
    /// implementation.
    #[structopt(display_order = 100)]
    Version {
        /// Returns name and version of the primary underlying OpenPGP
        /// toolkit.
        #[structopt(long, conflicts_with("extended"))]
        backend: bool,

        /// Returns multiple lines of name and version information.
        ///
        /// The first line is the name and version of the SOP
        /// implementation, but the rest have no defined structure.
        #[structopt(long, conflicts_with("backend"))]
        extended: bool,
    },
    /// Generates a Secret Key.
    #[structopt(display_order = 200)]
    GenerateKey {
        /// Don't ASCII-armor output.
        #[structopt(long)]
        no_armor: bool,
        /// UserIDs for the generated key.
        userids: Vec<String>,
    },
    /// Extracts a Certificate from a Secret Key.
    #[structopt(display_order = 300)]
    ExtractCert {
        /// Don't ASCII-armor output.
        #[structopt(long)]
        no_armor: bool,
    },
    /// Creates Detached Signatures.
    #[structopt(display_order = 400)]
    Sign {
        /// Don't ASCII-armor output.
        #[structopt(long)]
        no_armor: bool,
        /// Sign binary data or UTF-8 text.
        #[structopt(default_value = "binary", long = "as")]
        as_: SignAs,
        /// Emit the digest algorithm used to the specified file.
        #[structopt(long)]
        micalg_out: Option<String>,
        /// Keys for signing.
        keys: Vec<String>,
    },
    /// Verifies Detached Signatures.
    #[structopt(display_order = 500)]
    Verify {
        /// Consider signatures before this date invalid.
        #[structopt(long, parse(try_from_str = parse_bound_round_down))]
        not_before: Option<DateTime<Utc>>,
        /// Consider signatures after this date invalid.
        #[structopt(long, parse(try_from_str = parse_bound_round_up))]
        not_after: Option<DateTime<Utc>>,
        /// Signatures to verify.
        signatures: String,
        /// Certs for verification.
        certs: Vec<String>,
    },
    /// Encrypts a Message.
    #[structopt(display_order = 600)]
    Encrypt {
        /// Don't ASCII-armor output.
        #[structopt(long)]
        no_armor: bool,
        /// Encrypt binary data, UTF-8 text, or MIME data.
        #[structopt(default_value = "binary", long = "as")]
        as_: EncryptAs,
        /// Encrypt with passwords.
        #[structopt(long, number_of_values = 1)]
        with_password: Vec<String>,
        /// Keys for signing.
        #[structopt(long, number_of_values = 1)]
        sign_with: Vec<String>,
        /// Encrypt for these certs.
        certs: Vec<String>,
    },
    /// Decrypts a Message.
    #[structopt(display_order = 700)]
    Decrypt {
        /// Write the session key here.
        #[structopt(long)]
        session_key_out: Option<String>,
        /// Try to decrypt with this session key.
        #[structopt(long, number_of_values = 1)]
        with_session_key: Vec<String>,
        /// Try to decrypt with this password.
        #[structopt(long, number_of_values = 1)]
        with_password: Vec<String>,
        /// Write verification result here.
        #[structopt(long)]
        verify_out: Option<String>,
        /// Certs for verification.
        #[structopt(long, number_of_values = 1)]
        verify_with: Vec<String>,
        /// Consider signatures before this date invalid.
        #[structopt(long, parse(try_from_str = parse_bound_round_down))]
        verify_not_before: Option<DateTime<Utc>>,
        /// Consider signatures after this date invalid.
        #[structopt(long, parse(try_from_str = parse_bound_round_up))]
        verify_not_after: Option<DateTime<Utc>>,
        /// Try to decrypt with these keys.
        keys: Vec<String>,
    },
    /// Converts binary OpenPGP data to ASCII.
    #[structopt(display_order = 800)]
    Armor {
        /// Indicates the kind of data.
        #[structopt(long, default_value = "auto")]
        label: ArmorLabel,
    },
    /// Converts ASCII OpenPGP data to binary.
    #[structopt(display_order = 900)]
    Dearmor {
    },
    /// Unsupported subcommand.
    #[structopt(external_subcommand)]
    Unsupported(Vec<String>),
}

/// Generates shell completions.
pub fn write_shell_completions<B, P>(binary_name: B,
                                     out_path: P)
                                     -> io::Result<()>
where B: AsRef<str>,
      P: AsRef<Path>,
{
    use structopt::clap::Shell;

    let binary_name = binary_name.as_ref();
    let out_path = out_path.as_ref();
    std::fs::create_dir_all(&out_path)?;

    let mut clap = Operation::clap();
    for shell in &[Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell,
                   Shell::Elvish] {
        clap.gen_completions(binary_name, *shell, out_path);
    }
    Ok(())
}

/// Implements the SOP command-line interface.
pub fn main(sop: &dyn crate::SOP) -> ! {
    use std::process::exit;

    match real_main(sop) {
        Ok(()) => exit(0),
        Err(e) => {
            print_error_chain(&e);
            if let Ok(e) = e.downcast::<Error>() {
                exit(e.into())
            }
            exit(1);
        },
    }
}

fn real_main(sop: &dyn crate::SOP) -> Result<()> {
    // Do a little dance to supply name and version to structopt.
    let v = sop.version()?.frontend()?;
    let version = v.version.clone();
    let about = format!("An implementation of the Stateless OpenPGP Command \
                        Line Interface using {} {}",
                        v.name, v.version);
    let app = Operation::clap()
        .name(format!("SOP {}", v.name))
        .about(&about[..])
        .version(&version[..]);

    match Operation::from_clap(&app.get_matches()) {
        Operation::Version { backend, extended, } => {
            let version = sop.version()?;
            match (backend, extended) {
                (false, false) => {
                    let v = version.frontend()?;
                    println!("{} {}", v.name, v.version);
                },
                (true, false) => {
                    let v = version.backend()?;
                    println!("{} {}", v.name, v.version);
                },
                (false, true) => {
                    for v in version.extended()? {
                        println!("{} {}", v.name, v.version);
                    }
                },
                (true, true) => unreachable!("mutually exclusive"),
            }
        },

        Operation::GenerateKey { no_armor, userids, } => {
            let mut op = sop.generate_key()?;
            if no_armor {
                op = op.no_armor();
            }
            for u in userids {
                op = op.userid(&u);
            }
            op.generate()?.write_to(&mut io::stdout())?;
        },

        Operation::ExtractCert { no_armor, } => {
            let mut op = sop.extract_cert()?;
            if no_armor {
                op = op.no_armor();
            }
            op.keys(&mut io::stdin())?.write_to(&mut io::stdout())?;
        },

        Operation::Sign { no_armor, as_, micalg_out, keys, } => {
            let mut op = sop.sign()?.mode(as_);
            if no_armor {
                op = op.no_armor();
            }
            for mut key in load_files(keys)? {
                op = op.key(&mut key)?;
            }
            let micalg =
                op.data(&mut io::stdin())?.write_to(&mut io::stdout())?;
            if let Some(path) = micalg_out {
                let mut sink = create_file(path)?;
                write!(sink, "{}", micalg)?;
            }
        },

        Operation::Verify { not_before, not_after, signatures, certs, } => {
            let mut op = sop.verify()?;
            if let Some(t) = not_before {
                op = op.not_before(t.into());
            }
            if let Some(t) = not_after {
                op = op.not_after(t.into());
            }
            for mut cert in load_files(certs)? {
                op = op.certs(&mut cert)?;
            }
            let verifications = op.signatures(&mut load_file(signatures)?)?
                .data(&mut io::stdin())?;

            for v in verifications {
                println!("{}", v);
            }
        },

        Operation::Encrypt { no_armor, as_, with_password, sign_with, certs, } =>
        {
            let mut op = sop.encrypt()?.mode(as_);
            if no_armor {
                op = op.no_armor();
            }
            for mut key in load_files(sign_with)? {
                op = op.sign_with_key(&mut key)?;
            }
            for mut stream in load_files(with_password)? {
                let mut pw = String::new();
                stream.read_to_string(&mut pw)?;
                op = op.with_password(pw.into())?;
            }
            for mut cert in load_files(certs)? {
                op = op.with_cert(&mut cert)?;
            }
            op.plaintext(&mut io::stdin())?.write_to(&mut io::stdout())?;
        },

        Operation::Decrypt {
            session_key_out,
            with_session_key,
            with_password,
            verify_out,
            verify_with,
            verify_not_before,
            verify_not_after,
            keys,
        } => {
            let session_key_out: Option<Box<dyn io::Write>> =
                if let Some(f) = session_key_out {
                    Some(Box::new(create_file(f)?))
                } else {
                    None
                };

            if verify_out.is_none() != verify_with.is_empty() {
                return Err(anyhow::Error::from(Error::IncompleteVerification))
                    .context("--verify-out and --verify-with \
                              must both be given");
            }

            let mut verify_out: Box<dyn io::Write> =
                if let Some(f) = verify_out {
                    Box::new(create_file(f)?)
                } else {
                    Box::new(io::sink())
                };

            let mut op = sop.decrypt()?;

            for mut stream in load_files(with_session_key)? {
                let mut sk = String::new();
                stream.read_to_string(&mut sk)?;
                op = op.with_session_key(sk.parse()?)?;
            }

            for mut stream in load_files(with_password)? {
                let mut pw = String::new();
                stream.read_to_string(&mut pw)?;
                op = op.with_password(pw.into())?;
            }

            for mut key in load_files(keys)? {
                op = op.with_keys(&mut key)?;
            }

            for mut cert in load_files(verify_with)? {
                op = op.verify_with_certs(&mut cert)?;
            }

            if let Some(t) = verify_not_before {
                op = op.verify_not_before(t.into());
            }

            if let Some(t) = verify_not_after {
                op = op.verify_not_after(t.into());
            }

            let (session_key, verifications) =
                op.ciphertext(&mut io::stdin())?.write_to(&mut io::stdout())?;

            for v in verifications {
                writeln!(verify_out, "{}", v)?;
            }
            if let Some(mut sko) = session_key_out {
                if let Some(sk) = session_key {
                    writeln!(sko, "{}", sk)?;
                } else {
                    return Err(Error::UnsupportedSubcommand.into());
                }
            }
        },

        Operation::Armor { label, } => {
            let op = sop.armor()?.label(label);
            op.data(&mut io::stdin())?
                .write_to(&mut io::stdout())?;
        },

        Operation::Dearmor {} => {
            let op = sop.dearmor()?;
            op.data(&mut io::stdin())?
                .write_to(&mut io::stdout())?;
        },

        Operation::Unsupported(args) => {
            return Err(anyhow::Error::from(Error::UnsupportedSubcommand))
                .context(format!("Subcommand {} is not supported", args[0]));
        },
    }
    Ok(())
}

fn is_special_designator<S: AsRef<str>>(file: S) -> bool {
    file.as_ref().starts_with("@")
}

/// Loads the given (special) file.
fn load_file<S: AsRef<str>>(file: S)
                            -> Result<Box<dyn io::Read + Send + Sync>> {
    let f = file.as_ref();

    if is_special_designator(f) {
        if Path::new(f).exists() {
            return Err(anyhow::Error::from(Error::AmbiguousInput))
                .context(format!("File {:?} exists", f));
        }

        #[cfg(unix)]
        {
            if f.starts_with("@FD:")
                && f[4..].chars().all(|c| c.is_ascii_digit())
            {
                use std::os::unix::io::{RawFd, FromRawFd};

                let fd: RawFd = f[4..].parse()
                    .map_err(|_| Error::UnsupportedSpecialPrefix)?;
                let f = unsafe {
                    std::fs::File::from_raw_fd(fd)
                };
                return Ok(Box::new(f));
            }

            if f.starts_with("@ENV:") {
                use std::os::unix::ffi::OsStringExt;

                let key = &f[5..];
                let value = std::env::var_os(key)
                    .ok_or(Error::UnsupportedSpecialPrefix)?;
                // Prevent leak to child processes.
                std::env::remove_var(key);
                return Ok(Box::new(io::Cursor::new(value.into_vec())));
            }
        }

        #[cfg(windows)]
        {
            if f.starts_with("@ENV:") {
                let key = &f[5..];
                let value = std::env::var(key)
                    .map_err(|_| Error::UnsupportedSpecialPrefix)?;
                // Prevent leak to child processes.
                std::env::remove_var(key);
                return Ok(Box::new(io::Cursor::new(value.into_bytes())));
            }
        }

        return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix));
    }

    std::fs::File::open(f)
        .map(|f| -> Box<dyn io::Read + Send + Sync> { Box::new(f) })
        .map_err(|_| Error::MissingInput)
        .context(format!("Failed to open file {:?}", f))
}

/// Creates the given (special) file.
fn create_file<S: AsRef<str>>(file: S) -> Result<std::fs::File> {
    let f = file.as_ref();

    if is_special_designator(f) {
        if Path::new(f).exists() {
            return Err(anyhow::Error::from(Error::AmbiguousInput))
                .context(format!("File {:?} exists", f));
        }

        return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix));
    }

    if Path::new(f).exists() {
        return Err(anyhow::Error::from(Error::OutputExists))
            .context(format!("File {:?} exists", f));
    }

    std::fs::File::create(f).map_err(|_| Error::MissingInput) // XXX
            .context(format!("Failed to create file {:?}", f))
}

/// Loads the the (special) files.
fn load_files(files: Vec<String>)
              -> Result<Vec<Box<dyn io::Read + Send + Sync>>> {
    files.iter().map(load_file).collect()
}

/// Parses the given string depicting a ISO 8601 timestamp, rounding down.
fn parse_bound_round_down(s: &str) -> Result<DateTime<Utc>> {
    match s {
        // XXX: parse "-" to None once we figure out how to do that
        // with structopt.
        "now" => Ok(Utc::now()),
        _ => parse_iso8601(s, chrono::NaiveTime::from_hms(0, 0, 0)),
    }
}

/// Parses the given string depicting a ISO 8601 timestamp, rounding up.
fn parse_bound_round_up(s: &str) -> Result<DateTime<Utc>> {
    match s {
        // XXX: parse "-" to None once we figure out how to do that
        // with structopt.
        "now" => Ok(Utc::now()),
        _ => parse_iso8601(s, chrono::NaiveTime::from_hms(23, 59, 59)),
    }
}

/// Parses the given string depicting a ISO 8601 timestamp.
fn parse_iso8601(s: &str, pad_date_with: chrono::NaiveTime)
                 -> Result<DateTime<Utc>>
{
    // If you modify this function this function, synchronize the
    // changes with the copy in sqv.rs!
    for f in &[
        "%Y-%m-%dT%H:%M:%S%#z",
        "%Y-%m-%dT%H:%M:%S",
        "%Y-%m-%dT%H:%M%#z",
        "%Y-%m-%dT%H:%M",
        "%Y-%m-%dT%H%#z",
        "%Y-%m-%dT%H",
        "%Y%m%dT%H%M%S%#z",
        "%Y%m%dT%H%M%S",
        "%Y%m%dT%H%M%#z",
        "%Y%m%dT%H%M",
        "%Y%m%dT%H%#z",
        "%Y%m%dT%H",
    ] {
        if f.ends_with("%#z") {
            if let Ok(d) = DateTime::parse_from_str(s, *f) {
                return Ok(d.into());
            }
        } else {
            if let Ok(d) = chrono::NaiveDateTime::parse_from_str(s, *f) {
                return Ok(DateTime::from_utc(d, Utc));
            }
        }
    }
    for f in &[
        "%Y-%m-%d",
        "%Y-%m",
        "%Y-%j",
        "%Y%m%d",
        "%Y%m",
        "%Y%j",
        "%Y",
    ] {
        if let Ok(d) = chrono::NaiveDate::parse_from_str(s, *f) {
            return Ok(DateTime::from_utc(d.and_time(pad_date_with), Utc));
        }
    }
    Err(anyhow::anyhow!("Malformed ISO8601 timestamp: {}", s))
}

#[test]
fn test_parse_iso8601() {
    let z = chrono::NaiveTime::from_hms(0, 0, 0);
    parse_iso8601("2017-03-04T13:25:35Z", z).unwrap();
    parse_iso8601("2017-03-04T13:25:35+08:30", z).unwrap();
    parse_iso8601("2017-03-04T13:25:35", z).unwrap();
    parse_iso8601("2017-03-04T13:25Z", z).unwrap();
    parse_iso8601("2017-03-04T13:25", z).unwrap();
    // parse_iso8601("2017-03-04T13Z", z).unwrap(); // XXX: chrono doesn't like
    // parse_iso8601("2017-03-04T13", z).unwrap(); // ditto
    parse_iso8601("2017-03-04", z).unwrap();
    // parse_iso8601("2017-03", z).unwrap(); // ditto
    parse_iso8601("2017-031", z).unwrap();
    parse_iso8601("20170304T132535Z", z).unwrap();
    parse_iso8601("20170304T132535+0830", z).unwrap();
    parse_iso8601("20170304T132535", z).unwrap();
    parse_iso8601("20170304T1325Z", z).unwrap();
    parse_iso8601("20170304T1325", z).unwrap();
    // parse_iso8601("20170304T13Z", z).unwrap(); // ditto
    // parse_iso8601("20170304T13", z).unwrap(); // ditto
    parse_iso8601("20170304", z).unwrap();
    // parse_iso8601("201703", z).unwrap(); // ditto
    parse_iso8601("2017031", z).unwrap();
    // parse_iso8601("2017", z).unwrap(); // ditto
}

/// Errors defined by the Stateless OpenPGP Command-Line Protocol.
#[derive(thiserror::Error, Debug)]
enum Error {
    /// No acceptable signatures found ("sop verify").
    #[error("No acceptable signatures found")]
    NoSignature,

    /// Asymmetric algorithm unsupported ("sop encrypt").
    #[error("Asymmetric algorithm unsupported")]
    UnsupportedAsymmetricAlgo,

    /// Certificate not encryption-capable (e.g., expired, revoked,
    /// unacceptable usage flags) ("sop encrypt").
    #[error("Certificate not encryption-capable")]
    CertCannotEncrypt,

    /// Key not signature-capable (e.g., expired, revoked,
    /// unacceptable usage flags) (`sop sign` and `sop encrypt` with
    /// `--sign-with`).
    #[error("Key not signing-capable")]
    KeyCannotSign,

    /// Missing required argument.
    #[error("Missing required argument")]
    MissingArg,

    /// Incomplete verification instructions ("sop decrypt").
    #[error("Incomplete verification instructions")]
    IncompleteVerification,

    /// Unable to decrypt ("sop decrypt").
    #[error("Unable to decrypt")]
    CannotDecrypt,

    /// Non-"UTF-8" or otherwise unreliable password ("sop encrypt").
    #[error("Non-UTF-8 or otherwise unreliable password")]
    PasswordNotHumanReadable,

    /// Unsupported option.
    #[error("Unsupported option")]
    UnsupportedOption,

    /// Invalid data type (no secret key where "KEY" expected, etc).
    #[error("Invalid data type")]
    BadData,

    /// Non-text input where text expected.
    #[error("Non-text input where text expected")]
    ExpectedText,

    /// Output file already exists.
    #[error("Output file already exists")]
    OutputExists,

    /// Input file does not exist.
    #[error("Input file does not exist")]
    MissingInput,

    /// A "KEY" input is protected (locked) with a password, and "sop" cannot
    /// unlock it.
    #[error("A KEY input is protected with a password")]
    KeyIsProtected,

    /// Unsupported subcommand.
    #[error("Unsupported subcommand")]
    UnsupportedSubcommand,

    /// An indirect parameter is a special designator (it starts with "@") but
    /// "sop" does not know how to handle the prefix.
    #[error("An indirect parameter is a special designator with unknown prefix")]
    UnsupportedSpecialPrefix,

    /// A indirect input parameter is a special designator (it starts with
    /// "@"), and a filename matching the designator is actually present.
    #[error("A indirect input parameter is a special designator matches file")]
    AmbiguousInput,

    /// An I/O operation failed.
    #[error("I/O operation failed")]
    IoError(#[source] io::Error),
}

impl From<crate::Error> for Error {
    fn from(e: crate::Error) -> Self {
        use crate::Error as E;
        use Error as CE;
        match e {
            E::NoSignature => CE::NoSignature,
            E::UnsupportedAsymmetricAlgo => CE::UnsupportedAsymmetricAlgo,
            E::CertCannotEncrypt => CE::CertCannotEncrypt,
            E::KeyCannotSign => CE::KeyCannotSign,
            E::MissingArg => CE::MissingArg,
            E::IncompleteVerification => CE::IncompleteVerification,
            E::CannotDecrypt => CE::CannotDecrypt,
            E::PasswordNotHumanReadable => CE::PasswordNotHumanReadable,
            E::UnsupportedOption => CE::UnsupportedOption,
            E::BadData => CE::BadData,
            E::ExpectedText => CE::ExpectedText,
            E::OutputExists => CE::OutputExists,
            E::MissingInput => CE::MissingInput,
            E::KeyIsProtected => CE::KeyIsProtected,
            E::AmbiguousInput => CE::AmbiguousInput,
            E::NotImplemented => CE::UnsupportedSubcommand,
            E::IoError(e) => CE::IoError(e),
        }
    }
}

impl From<Error> for i32 {
    fn from(e: Error) -> Self {
        use Error::*;
        match e {
            NoSignature => 3,
            UnsupportedAsymmetricAlgo => 13,
            CertCannotEncrypt => 17,
            MissingArg => 19,
            IncompleteVerification => 23,
            CannotDecrypt => 29,
            PasswordNotHumanReadable => 31,
            UnsupportedOption => 37,
            BadData => 41,
            ExpectedText => 53,
            OutputExists => 59,
            MissingInput => 61,
            KeyIsProtected => 67,
            UnsupportedSubcommand => 69,
            UnsupportedSpecialPrefix => 71,
            AmbiguousInput => 73,
            KeyCannotSign => 79,
            IoError(_) => 1,
        }
    }
}

/// Prints the error and causes, if any.
fn print_error_chain(err: &anyhow::Error) {
    eprintln!("           {}", err);
    err.chain().skip(1).for_each(|cause| eprintln!("  because: {}", cause));
}
