//! A simple signature verification program.
//!
//! See <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=872271> for
//! the motivation.

use std::borrow::Cow;
use std::io::Read;
use std::io;
use std::path::PathBuf;
use std::process::exit;
use std::sync::OnceLock;

use chrono::{DateTime, offset::Utc};
use anyhow::Context;

use clap::FromArgMatches;

use sequoia_openpgp as openpgp;

use crate::openpgp::{
    Cert,
    KeyHandle,
    Packet,
    Result,
    parse::PacketParser,
    parse::PacketParserResult,
    parse::Parse,
};
use crate::openpgp::parse::stream::{
    DetachedVerifierBuilder,
    MessageLayer,
    MessageStructure,
    VerificationHelper,
    GoodChecksum,
    VerificationError,
    VerifierBuilder,
};
use crate::openpgp::cert::prelude::*;

mod cli;

/// Returns stdin.
///
/// It is standard to `-` to mean read from `stdin` instead of a file.
/// Because it is almost always an error to use `-` more than once,
/// this errors out with a helpful error message in this case.
fn stdin(option: &'static str) -> Result<std::io::Stdin> {
    static OPTION: OnceLock<&'static str> = OnceLock::new();

    match OPTION.set(option) {
        Ok(()) => {
            Ok(std::io::stdin())
        }
        Err(_) => {
            let previous = OPTION.get().unwrap();
            Err(anyhow::anyhow!(
                "Can't use - to read from stdin multiple times \
                 (used with {} and {})",
                previous, option))
        }
    }
}

struct VHelper {
    not_before: Option<std::time::SystemTime>,
    not_after: std::time::SystemTime,

    good: usize,
    total: usize,
    threshold: usize,

    keyrings: Vec<PathBuf>,
}

impl std::fmt::Debug for VHelper {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        f.debug_struct("VHelper")
            .field("not_before", &self.not_before)
            .field("not_after", &self.not_after)
            .field("good", &self.good)
            .field("total", &self.total)
            .field("threshold", &self.threshold)
            .field("keyrings", &self.keyrings)
            .finish()
    }
}

impl VHelper {
    fn new(threshold: usize,
           not_before: Option<std::time::SystemTime>,
           not_after: std::time::SystemTime,
           keyrings: Vec<PathBuf>) -> Self {
        VHelper {
            not_before: not_before,
            not_after: not_after,
            good: 0,
            total: 0,
            threshold: threshold,
            keyrings: keyrings,
        }
    }
}

impl VerificationHelper for VHelper {
    fn get_certs(&mut self, ids: &[crate::KeyHandle]) -> Result<Vec<Cert>> {
        let mut certs = Vec::with_capacity(ids.len());

        // Load relevant keys from the keyring.
        for filename in self.keyrings.iter() {
            let parser = if filename.as_os_str().as_encoded_bytes() == b"-" {
                CertParser::from_reader(stdin("--keyring")?)
            } else {
                CertParser::from_file(filename)
            };
            let parser = parser
                .map_err(|err| {
                    let Ok(mut file) = std::fs::File::open(filename) else {
                        return err;
                    };
                    let mut buffer = [0; 0x0C];
                    let Ok(()) = file.read_exact(&mut buffer) else {
                        return err;
                    };
                    if buffer.ends_with(b"KBXf") {
                        anyhow::anyhow!("File appears to be in keybox format, \
                                         which is not supported")
                    } else {
                        err
                    }
                })
                .with_context(|| format!("Failed to parse keyring {:?}",
                                         filename))?;
            for cert in parser
                .unvalidated_cert_filter(|cert, _| {
                    // We don't skip keys that are valid (not revoked,
                    // alive, etc.) so that
                    cert.keys().key_handles(ids.iter()).next().is_some()
                })
            {
                certs.push(cert.with_context(|| {
                    format!("Malformed certificate in keyring {:?}", filename)
                })?);
            }
        }

        // Dedup.  To avoid cloning the certificates, we don't use
        // Vec::dedup.
        certs.sort_by(|a, b| a.fingerprint().cmp(&b.fingerprint()));
        let count = certs.len();
        let (certs, errs) = certs.into_iter().fold(
            (Vec::with_capacity(count), Vec::new()),
            |(mut certs, mut errs), a| {
                if certs.is_empty() {
                    certs.push(a);
                } else if certs[certs.len() - 1].fingerprint() == a.fingerprint() {
                    // Merge `a` into the last element.
                    match certs.pop().expect("non-empty vec").merge_public(a) {
                        Ok(cert) => certs.push(cert),
                        Err(err) => errs.push(err),
                    }
                } else {
                    certs.push(a);
                }

                (certs, errs)
            });

        if !errs.is_empty() {
            eprintln!("Error merging duplicate keys:");
            for err in errs.iter() {
                eprintln!("  {}", err);
            }
            Err(errs.into_iter().next().expect("non-empty vec"))
        } else {
            Ok(certs)
        }
    }

    fn check(&mut self, structure: MessageStructure) -> Result<()> {
        use self::VerificationError::*;

        let mut signers = Vec::with_capacity(2);
        let mut verification_err = None;

        for layer in structure.into_iter() {
            match layer {
                MessageLayer::SignatureGroup { results } =>
                    for result in results {
                        self.total += 1;
                        match result {
                            Ok(GoodChecksum { sig, ka, .. }) => {
                                match (sig.signature_creation_time(),
                                                self.not_before,
                                                self.not_after)
                                {
                                    (None, _, _) => {
                                        eprintln!("Malformed signature:");
                                        print_error_chain(&anyhow::anyhow!(
                                            "no signature creation time"));
                                    },
                                    (Some(t), Some(not_before), not_after) => {
                                        if t < not_before {
                                            eprintln!(
                                                "Signature by {:X} was created before \
                                                 the --not-before date.",
                                                ka.key().fingerprint());
                                        } else if t > not_after {
                                            eprintln!(
                                                "Signature by {:X} was created after \
                                                 the --not-after date.",
                                                ka.key().fingerprint());
                                        } else {
                                            signers.push(ka.cert().fingerprint());
                                        }
                                    }
                                    (Some(t), None, not_after) => {
                                        if t > not_after {
                                            eprintln!(
                                                "Signature by {:X} was created after \
                                                 the --not-after date.",
                                                ka.key().fingerprint());
                                        } else {
                                            signers.push(ka.cert().fingerprint());
                                        }
                                    }
                                };
                            }
                            Err(MalformedSignature { error, .. }) => {
                                eprintln!("Signature is malformed:");
                                print_error_chain(&error);
                            }
                            Err(MissingKey { sig, .. }) => {
                                let issuers = sig.get_issuers();
                                eprintln!("Missing key {}, which is needed to \
                                           verify signature.",
                                          issuers
                                              .first()
                                              .map(|issuer| {
                                                  Cow::Owned(issuer.to_string())
                                              })
                                              .unwrap_or_else(|| {
                                                  Cow::Borrowed("<unknown issuer>")
                                              }));
                            }
                            Err(UnboundKey { cert, error, .. }) => {
                                eprintln!("Signing key on {:X} is not bound:",
                                          cert.fingerprint());
                                print_error_chain(&error);
                            }
                            Err(BadKey { ka, error, .. }) => {
                                eprintln!("Signing key on {:X} is bad:",
                                          ka.cert().fingerprint());
                                print_error_chain(&error);
                            }
                            Err(BadSignature { error, .. }) => {
                                eprintln!("Verifying signature:");
                                print_error_chain(&error);
                                if verification_err.is_none() {
                                    verification_err = Some(error)
                                }
                            }

                            Err(UnknownSignature { sig, .. }) => {
                                eprintln!("Verifying signature:");
                                print_error_chain(sig.error());
                                if verification_err.is_none() {
                                    verification_err =
                                        Some(anyhow::anyhow!("{}", sig.error()));
                                }
                            }

                            Err(e) => {
                                eprintln!("Verifying signature: {}", e);
                                if verification_err.is_none() {
                                    verification_err =
                                        Some(anyhow::anyhow!("{}", e));
                                }
                            }
                        }
                    }
                MessageLayer::Compression { .. } => (),
                _ => unreachable!(),
            }
        }

        // Dedup the keys so that it is not possible to exceed the
        // threshold by duplicating signatures or by using the same
        // key.
        signers.sort();
        signers.dedup();

        self.good = signers.len();
        for signer in signers {
            println!("{:X}", signer);
        }

        Ok(())
    }
}

fn print_error_chain(err: &anyhow::Error) {
    eprintln!("           {}", err);
    err.chain().skip(1).for_each(|cause| eprintln!("  because: {}", cause));
}


fn main() -> Result<()> {
    let matches = cli::build().get_matches();
    let cli = cli::SqvCommand::from_arg_matches(&matches)?;

    if cli.data.is_none() && cli.detached.is_none()
        && ! cli.message && ! cli.cleartext
    {
        return Err(anyhow::anyhow!(
            "Either `--signature-file`, `--message`, or `--cleartext` \
             must be given."));
    }

    let verbose = cli.verbose;

    let good_threshold = cli.signatures;
    if good_threshold < 1 {
        eprintln!("Value passed to --signatures must be >= 1 (got: {:?}).",
                  good_threshold);
        exit(2);
    }

    // Parse the --time argument to get the session reference time.
    // If not given, defaults to the current time.
    let ref_time = cli.time.as_ref()
        .map(|at| at.to_system_time(std::time::SystemTime::now()))
        .transpose()
        .with_context(|| format!("Error parsing --time: {}",
                                 cli.time.as_ref().unwrap()))?
        .unwrap_or_else(std::time::SystemTime::now);

    let not_before: Option<std::time::SystemTime> =
        if let Some(t) = cli.not_before {
            Some(parse_iso8601(&t, chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
                 .context(format!("Bad value passed to --not-before: {:?}", t))?
                 .into())
        } else {
            None
        };
    let not_after: std::time::SystemTime =
        if let Some(t) = cli.not_after {
            Some(parse_iso8601(&t, chrono::NaiveTime::from_hms_opt(23, 59, 59).unwrap())
                 .context(format!("Bad value passed to --not-after: {:?}", t))?
                 .into())
        } else {
            None
        }.unwrap_or(ref_time);

    let h = VHelper::new(good_threshold, not_before, not_after, cli.keyring);

    let t = cli.policy_as_of.as_ref()
        .map(|at| at.to_system_time(ref_time))
        .transpose()
        .with_context(|| format!("Error parsing --policy-as-of: {}",
                                 cli.policy_as_of.unwrap()))?
        .unwrap_or(ref_time);

    let mut p = sequoia_policy_config::ConfiguredStandardPolicy::at(t);
    p.parse_default_config()?;
    let p = p.build();

    // Dispatch.
    let h = if cli.data.is_some() || cli.detached.is_some() {
        let sig = cli.detached.as_ref().unwrap_or(&cli.file);
        let data = cli.data.as_ref().unwrap_or(&cli.file);

        let v = if sig.as_os_str().as_encoded_bytes() == b"-" {
            DetachedVerifierBuilder::from_reader(stdin("--signature-file")?)?
        } else {
            DetachedVerifierBuilder::from_file(sig)?
        };
        let mut v = v.with_policy(&p, Some(ref_time), h)
            .map_err(|err| {
                // Check if the user reversed the arguments.
                if data.as_os_str().as_encoded_bytes() != b"-" {
                    if let Ok(ppr) = PacketParser::from_file(data) {
                        if let PacketParserResult::Some(pp) = ppr {
                            if let Packet::Signature(_) = pp.packet {
                                eprintln!(
                                    "Hint: Data file ({}) appears to contain \
                                     an OpenPGP signature, perhaps you \
                                     reversed the arguments.",
                                    data.display());
                            }
                        }
                    }
                }
                err
            })?;
        if data.as_os_str().as_encoded_bytes() == b"-" {
            v.verify_reader(stdin("FILE")?)?;
        } else {
            v.verify_file(data)?;
        }
        v.into_helper()
    } else {
        assert!(cli.message || cli.cleartext);
        let mut sink: Box<dyn io::Write> = if let Some(n) = &cli.output {
            let mut overwrite = cli.overwrite;

            // Imply --overwrite if writing to a special file.
            #[cfg(unix)]
            if ! overwrite {
                use std::os::unix::fs::FileTypeExt;

                if let Ok(metadata) = std::fs::metadata(n) {
                    let ft = metadata.file_type();
                    if ft.is_char_device()
                        || ft.is_block_device()
                        || ft.is_fifo()
                        || ft.is_socket()
                    {
                        overwrite = true;
                    }
                }
            }

            Box::new(std::fs::File::options()
                .write(true)
                .create(overwrite) // choose create() when allowing overwrite
                .create_new(!overwrite) // else insist on new file
                .open(n)?)
        } else {
            Box::new(io::stdout())
        };

        let v = if cli.file.as_os_str().as_encoded_bytes() == b"-" {
            VerifierBuilder::from_reader(stdin("FILE")?)?
        } else {
            VerifierBuilder::from_file(&cli.file)?
        };
        let mut v = v.with_policy(&p, Some(ref_time), h)?;
        io::copy(&mut v, &mut sink)?;
        v.into_helper()
    };

    if verbose {
        eprintln!("{} of {} signatures are valid (threshold is: {}).",
                  h.good, h.total, good_threshold);
    }

    exit(if h.good >= good_threshold { 0 } else { 1 });
}

/// 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_naive_utc_and_offset(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_naive_utc_and_offset(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_opt(0, 0, 0).unwrap();
    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
}
