// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Handling of OpenPGP signatures.

use std::io;
use std::ops::Add;

use chrono::{DateTime, Utc};
use pgp::packet::{Packet, RevocationCode, SignatureType, SubpacketData};
use pgp::ser::Serialize;
use pgp::{Deserializable, Signature, StandaloneSignature};

use crate::policy::{accept_for_signatures, acceptable_hash_algorithm};
use crate::Error;

fn is_revocation(sig: &Signature) -> bool {
    match sig.typ() {
        SignatureType::KeyRevocation
        | SignatureType::CertRevocation
        | SignatureType::SubkeyRevocation => {
            // this is a revocation

            true // unless explicitly marked as "soft" (by the reason code), we consider a revocation to be "hard"
        }
        _ => false, // this signature is not a revocation
    }
}

fn is_hard_revocation(sig: &Signature) -> bool {
    // FIXME: DRY with is_revocation
    match sig.typ() {
        SignatureType::KeyRevocation
        | SignatureType::CertRevocation
        | SignatureType::SubkeyRevocation => {
            // this is a revocation, but is it a hard revocation?

            match sig.revocation_reason_code() {
                Some(RevocationCode::KeyRetired)
                | Some(RevocationCode::CertUserIdInvalid)
                | Some(RevocationCode::KeySuperseded) => {
                    return false; // these are soft revocation codes
                }
                _ => {}
            }

            true // unless explicitly marked as "soft" (by the reason code), we consider a revocation to be "hard"
        }
        _ => false, // this signature is not a revocation
    }
}

/// Reports if a signature meets basic acceptability criteria, including the cryptographic
/// primitives it uses:
///
/// - Rejects signatures that have no creation time stamp set in the hashed area
/// - Rejects signatures that have a creation time stamp in the future
/// - Rejects signatures that contain unknown critical subpackets
///
/// - Rejects signatures based on weak cryptographic mechanisms, based on (claimed) signature creation time:
///
/// md5 hashes are considered broken (effective January 1, 2010, based on the signature creation time).
/// We consider a more recently dated md5-based signature equally broken as one with an invalid cryptographic hash digest.
///
/// sha1 hashes for data signatures are considered broken effective January 1, 2014;
/// sha1 hashes for other signature types are considered broken effective February 1, 2023.
///
/// DSA signatures are considered unacceptable starting February 3, 2023.
///
/// (Rejecting signatures that are technically correct, but use broken primitives is a defensive
/// tradeoff: we consider legacy signatures that were made after a cutoff time as either attacks or
/// mistakes.)
pub(crate) fn signature_acceptable(sig: &Signature) -> bool {
    // FIXME: break this fn up, it does too many different things

    let Some(sig_creation_time) = sig.created() else {
        return false;
    };

    // A signature with a future creation time is not currently valid
    let now: DateTime<Utc> = chrono::offset::Utc::now();
    if *sig_creation_time > now {
        return false;
    }

    // critical unknown subpackets or notations invalidate a signature
    for sp in &sig.config.hashed_subpackets {
        if sp.is_critical {
            if let SubpacketData::Notation(_notation) = &sp.data {
                // Unknown critical notation (by default)
                // FIXME: how would an application use critical notations? initialize rpgpie with a good-list?
                return false;
            }
        }
    }

    let data_sig = sig.typ() == SignatureType::Binary || sig.typ() == SignatureType::Text;

    // reject signature if our policy rejects the hash algorithm at signature creation time
    if !acceptable_hash_algorithm(&sig.config.hash_alg, sig_creation_time, data_sig) {
        return false;
    }

    // reject signature if our policy rejects the signature's public key algorithm at creation time
    if !accept_for_signatures(sig.config.pub_alg, sig_creation_time) {
        return false;
    }

    true
}

pub(crate) fn not_older_than(sig: &Signature, created: &DateTime<Utc>) -> bool {
    sig.created()
        .is_some_and(|sig_created| sig_created >= created)
}

/// How long is `sig` valid?
///
/// Some(dt): valid until `dt`
/// None: unlimited validity
///
/// NOTE: Also returns `None` if `sig` has no creation time.
pub(crate) fn validity_end(sig: &Signature) -> Option<DateTime<Utc>> {
    let Some(sig_creation) = sig.created() else {
        // This is an error case, but we don't handle it here.
        // Callers are expected to handle it independently of this fn.
        return None;
    };

    if let Some(sig_expiration) = sig.signature_expiration_time() {
        if sig_expiration.num_seconds() != 0 {
            Some(sig_creation.add(*sig_expiration))
        } else {
            None
        }
    } else {
        None
    }
}

/// Check temporal validity of a signature
pub(crate) fn is_signature_valid_at(
    sig: &Signature,
    key_creation: &DateTime<Utc>,
    reference: &DateTime<Utc>,
) -> bool {
    if let Some(creation) = sig.created() {
        // If the signature is created after the reference time, it is invalid at reference time
        if creation > reference {
            return false;
        }

        // If the signature expires before the reference time, the signature is invalid
        if let Some(sig_exp) = validity_end(sig) {
            if sig_exp < *reference {
                return false;
            }
        }

        // If the key expires and expiration is before the reference time, the signature is invalid
        if let Some(key_exp) = sig.key_expiration_time() {
            if key_exp.num_seconds() != 0 && (key_creation.add(*key_exp) < *reference) {
                return false;
            }
        }

        true
    } else {
        // A signature with unset creation time is invalid
        false
    }
}

/// Read a list of Signatures from an input
pub fn load<R: io::Read>(source: &mut R) -> Result<Vec<Signature>, Error> {
    let (iter, _header) = StandaloneSignature::from_reader_many(source)?;

    let mut sigs = vec![];

    for res in iter {
        match res {
            Ok(sig) => sigs.push(sig.signature),
            Err(e) => return Err(Error::Message(format!("Bad data: {:?}", e))),
        }
    }

    Ok(sigs)
}

/// Write a list of Signatures to an output
pub fn save(
    signatures: &[Signature],
    armored: bool,
    mut sink: &mut dyn io::Write,
) -> Result<(), Error> {
    if armored {
        let packets: Vec<_> = signatures.iter().map(|s| Packet::from(s.clone())).collect();

        pgp::armor::write(
            &packets,
            pgp::armor::BlockType::Signature,
            &mut sink,
            None,
            true,
        )?;
    } else {
        for s in signatures {
            let p = Packet::from(s.clone());
            p.to_writer(&mut sink)?;
        }
    }

    Ok(())
}

pub(crate) struct SigStack<'inner> {
    hard: Vec<&'inner Signature>,
    soft: Vec<&'inner Signature>,
    regular: Vec<&'inner Signature>,

    #[allow(dead_code)]
    invalid: Vec<&'inner Signature>,

    #[allow(dead_code)]
    third_party: Vec<&'inner Signature>,
}

impl<'a> FromIterator<&'a Signature> for SigStack<'a> {
    fn from_iter<T: IntoIterator<Item = &'a Signature>>(iter: T) -> Self {
        let mut hard = vec![];
        let mut soft = vec![];
        let mut regular = vec![];
        let mut invalid = vec![];
        let third_party = vec![];

        // FIXME: pre-process?
        // - Different Enum variants based on hard-revoked vs. not?
        // - Sort signatures by creation time?
        // - Calculate time spans for validity of the component?

        for sig in iter.into_iter() {
            if is_hard_revocation(sig) {
                hard.push(sig)
            } else if is_revocation(sig) {
                soft.push(sig)
            } else if signature_acceptable(sig) {
                regular.push(sig)
            } else {
                invalid.push(sig)

                // FIXME: handle third party sigs
            }
        }

        Self {
            hard,
            soft,
            regular,
            invalid,
            third_party,
        }
    }
}

impl<'a> SigStack<'a> {
    // FIXME: this is an odd hack, and kind of expensive to make.
    // This probably should be replaced with a more reasonable access mechanism.
    #[allow(dead_code)]
    pub fn all(&self) -> Vec<&Signature> {
        // TODO: always return two sets: "valid" and "invalid"?
        //
        // -> for hard revoked, all other sigs are "invalid" (because they are suspect and can't be
        // relied on, they are only of informational value)
        //
        // -> for non-hard-revoked, only "self.invalid" are considered "invalid"? (unsound
        // signatures of all kinds might be of interest to viewers, but shouldn't be relied on for
        // anything)

        let mut all: Vec<&Signature> = vec![];
        self.hard.iter().for_each(|&s| all.push(s));
        self.soft.iter().for_each(|&s| all.push(s));
        self.regular.iter().for_each(|&s| all.push(s));

        // FIXME: handle "invalid" sigs?

        // FIXME: handle third party sigs?

        // sort by creation, newest first
        // FIXME: missing creation times are not legal, do we want to handle them here?
        all.sort_by(|a, b| b.created().cmp(&a.created()));

        all
    }

    /// Get the latest active signature in this stack.
    pub fn active(&self) -> Option<&'a Signature> {
        self.active_at(None)
    }

    /// Get the currently active signature in this stack at a reference time.
    /// If the reference time is `None`, then the latest signature.
    pub(crate) fn active_at(&self, reference: Option<&DateTime<Utc>>) -> Option<&'a Signature> {
        log::debug!("search active_at: {:?}", reference);

        // If there is any hard revocation, that is always active
        // (if there are multiple, picking a specific one is probably pointless)
        if let Some(&hard) = self.hard.first() {
            log::debug!(
                " found hard: {}",
                hard.created()
                    .map(|dt| format!("{:?}", dt))
                    .unwrap_or("<no creation time>".to_string())
            );
            return Some(hard);
        }

        // If any soft revocations exist that are created <= `reference`, we return the latest of them.
        let mut latest_soft: Option<&Signature> = None;
        self.soft
            .iter()
            .filter(|&&s| {
                if let Some(reference) = reference {
                    if let Some(sig_created) = s.created() {
                        // only consider signatures that were created before the reference time
                        sig_created <= reference
                    } else {
                        false // signature has no creation time, we ignore it
                    }
                } else {
                    true
                }
            })
            .for_each(|s| {
                // replace "latest_soft" with any newer soft revocation
                if let Some(cur) = latest_soft {
                    if let Some(sig_created) = s.created() {
                        if let Some(cur_created) = cur.created() {
                            if cur_created < sig_created {
                                latest_soft = Some(s);
                            }
                        }
                    }
                } else {
                    latest_soft = Some(s);
                }
            });

        if let Some(latest_soft) = latest_soft {
            log::debug!(
                " found soft: {}",
                latest_soft
                    .created()
                    .map(|dt| format!("{:?}", dt))
                    .unwrap_or("<no creation time>".to_string())
            );
            return Some(latest_soft);
        }

        // Otherwise we return the latest regular signature, if any
        let mut latest: Option<&Signature> = None;

        self.regular
            .iter()
            .filter(|s| {
                if let Some(reference) = reference {
                    if let Some(expired) = validity_end(s) {
                        // only consider signatures that are valid longer than the reference time
                        expired > *reference
                    } else {
                        true
                    }
                } else {
                    true
                }
            })
            .filter(|&&s| {
                if let Some(reference) = reference {
                    if let Some(sig_created) = s.created() {
                        // only consider signatures that were created before the reference time
                        sig_created <= reference
                    } else {
                        false // signature has no creation time, we ignore it
                    }
                } else {
                    true
                }
            })
            .for_each(|s| {
                // replace "latest" with any newer Signatures
                if let Some(cur) = latest {
                    if let Some(sig_created) = s.created() {
                        if let Some(cur_created) = cur.created() {
                            if cur_created < sig_created {
                                latest = Some(s);
                            }
                        }
                    }
                } else {
                    latest = Some(s);
                }
            });

        if let Some(latest) = latest {
            log::debug!(
                " latest regular: {}",
                latest
                    .created()
                    .map(|dt| format!("{:?}", dt))
                    .unwrap_or("<no creation time>".to_string())
            );
        }

        latest
    }

    /// Does this signature stack contain a (non-revocation) Signature that is temporally valid at
    /// `reference`?
    pub fn has_valid_binding_at(
        &self,
        reference: &DateTime<Utc>,
        key_creation: &DateTime<Utc>, // FIXME: factor out?
    ) -> bool {
        // FIXME: we could search more efficiently in large stacks, if the stack were sorted (maybe later)
        self.regular
            .iter()
            .any(|s| is_signature_valid_at(s, key_creation, reference))
    }

    pub(crate) fn revoked_at(&self, reference: &DateTime<Utc>) -> bool {
        self.contains_hard_revocation() || self.soft_revoked_at(reference)
    }

    /// CAUTION: this fn doesn't consider hard revocations!
    fn soft_revoked_at(&self, reference: &DateTime<Utc>) -> bool {
        log::debug!("soft_revoked_at {:?}", reference);
        if self.soft.iter().any(|&s| match s.created() {
            None => false, // we just ignore signatures without creation time
            Some(created) => created <= reference,
        }) {
            return true; // We consider this revocation active
        }

        false
    }

    fn contains_hard_revocation(&self) -> bool {
        !self.hard.is_empty()
    }
}
