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

use std::io;

use pgp::{
    crypto::hash::HashAlgorithm,
    types::{Password, SigningKey},
};
use rpgpie::{message::SignatureMode, tsk::Tsk};

use crate::{Keys, RPGSOP, Sigs};

pub(crate) struct Sign {
    pub(crate) mode: sop::ops::SignAs,
    pub(crate) with_key_password: Vec<sop::Password>,
    pub(crate) signers: Vec<Tsk>,
}

impl Sign {
    pub(crate) fn new() -> Self {
        Self {
            mode: Default::default(),
            with_key_password: Default::default(),
            signers: Default::default(),
        }
    }
}

impl Sign {
    pub(crate) fn add_signing_keys(&mut self, keys: &Keys) -> sop::Result<()> {
        for key in &keys.keys {
            self.add_signing_key(key)?;
        }
        Ok(())
    }

    fn add_signing_key(&mut self, tsk: &Tsk) -> sop::Result<()> {
        self.signers.push(tsk.clone());

        Ok(())
    }
}

impl<'a> sop::ops::Sign<'a, RPGSOP, Keys, Sigs> for Sign {
    fn mode(
        mut self: Box<Self>,
        mode: sop::ops::SignAs,
    ) -> Box<dyn sop::ops::Sign<'a, RPGSOP, Keys, Sigs> + 'a> {
        self.mode = mode;
        self
    }

    fn keys(
        mut self: Box<Self>,
        keys: &Keys,
    ) -> sop::Result<Box<dyn sop::ops::Sign<'a, RPGSOP, Keys, Sigs> + 'a>> {
        self.add_signing_keys(keys)?;
        Ok(self)
    }

    fn with_key_password(
        mut self: Box<Self>,
        password: sop::Password,
    ) -> sop::Result<Box<dyn sop::ops::Sign<'a, RPGSOP, Keys, Sigs> + 'a>> {
        self.with_key_password.push(password);
        Ok(self)
    }

    fn data(
        self: Box<Self>,
        input: &mut (dyn io::Read + Send + Sync),
    ) -> sop::Result<(sop::ops::Micalg, Sigs)> {
        if self.signers.is_empty() {
            return Err(sop::errors::Error::MissingArg);
        }

        // Outer `Option`: If `None`, we have given up finding a matching hash algorithm
        // Inner `Option`: Once we made any signature, store the hash algorithm we used
        let mut micalg = MicalgOut::init();

        let mut data = vec![];
        input.read_to_end(&mut data)?;

        let mut sigs = vec![];

        // Passwords to try
        let pws: Vec<&[u8]> = if self.with_key_password.is_empty() {
            vec![&[]]
        } else {
            self.with_key_password
                .iter()
                .map(sop::plumbing::PasswordsAreHumanReadable::normalized)
                .collect()
        };

        for tsk in self.signers {
            for signer in tsk.signing_capable_component_keys() {
                log::info!(
                    "Trying to sign data with signer: {:02x?}",
                    signer.fingerprint()
                );

                let sig = pws
                    .iter()
                    .flat_map(|pw| {
                        let result = signer.sign_data(
                            &data,
                            match self.mode {
                                sop::ops::SignAs::Text => SignatureMode::Text,
                                sop::ops::SignAs::Binary => SignatureMode::Binary,
                            },
                            &Password::from(*pw),
                            signer.hash_alg(),
                        );

                        if result.is_err() {
                            log::warn!("Signing failed: {result:?}");
                        }

                        result
                    })
                    .next();

                match sig {
                    Some(s) => {
                        // update micalg output

                        if let Some(config) = s.config() {
                            micalg.made_a_signature(config.hash_alg);
                        }

                        sigs.push(s);
                    }
                    None => {
                        log::warn!(
                            "Couldn't sign with signer key {:02x?}",
                            signer.fingerprint()
                        );

                        // signing with this signing key failed but let's continue
                    }
                }
            }
        }

        if sigs.is_empty() {
            // FIXME: probably the password(s) were wrong, but this is a bit of a guess
            return Err(sop::errors::Error::KeyIsProtected);
        }

        Ok((
            micalg.get(),
            Sigs {
                sigs,
                source_name: None,
            },
        ))
    }
}

/// Helper to calculate the micalg return value
struct MicalgOut {
    seen_any_sig: bool,
    shared_hash_algo: Option<HashAlgorithm>,
}

impl MicalgOut {
    fn init() -> Self {
        Self {
            seen_any_sig: false,
            shared_hash_algo: None,
        }
    }

    /// The signer made a new signature with hash algorithm `hash`
    fn made_a_signature(&mut self, hash: HashAlgorithm) {
        if !self.seen_any_sig {
            // remember the first hash algorithm that we see
            self.shared_hash_algo = Some(hash);
            self.seen_any_sig = true;
        } else if self.shared_hash_algo != Some(hash) {
            // give up, the hashes use different algorithms
            self.shared_hash_algo = None;
        }
    }

    /// Get the calculated micalg return value for the used set of hash algorithms
    /// (empty, if not uniform)
    fn get(&self) -> sop::ops::Micalg {
        match self.shared_hash_algo {
            Some(alg) => u8::from(alg).into(),
            _ => sop::ops::Micalg::Unknown("".to_string()),
        }
    }
}
