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

use std::io;

use pgp::{
    composed::{ArmorOptions, CleartextSignedMessage, MessageBuilder},
    packet::Signature,
    types::{KeyDetails, KeyVersion, SigningKey},
};
use rpgpie::{message::SignatureMode, tsk::DataSigner};
use sop::errors::Error;

use crate::{Keys, RPGSOP, cmd::sign::Sign};

pub(crate) struct InlineSign {
    armor: bool,
    sign: Sign,
    mode: sop::ops::InlineSignAs,
}

impl InlineSign {
    pub(crate) fn new() -> Self {
        Self {
            armor: true,
            sign: Sign::new(),
            mode: Default::default(),
        }
    }
}

impl<'a> sop::ops::InlineSign<'a, RPGSOP, Keys> for InlineSign {
    fn no_armor(mut self: Box<Self>) -> Box<dyn sop::ops::InlineSign<'a, RPGSOP, Keys> + 'a> {
        self.armor = false;
        self
    }

    fn mode(
        mut self: Box<Self>,
        mode: sop::ops::InlineSignAs,
    ) -> Box<dyn sop::ops::InlineSign<'a, RPGSOP, Keys> + 'a> {
        self.mode = mode;
        self
    }

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

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

    fn data<'d>(
        self: Box<Self>,
        data: &'d mut (dyn io::Read + Send + Sync),
    ) -> sop::Result<Box<dyn sop::ops::Ready + 'd>>
    where
        'a: 'd,
    {
        if self.sign.signers.is_empty() {
            return Err(Error::MissingArg);
        }

        if !self.armor && matches!(self.mode, sop::ops::InlineSignAs::ClearSigned) {
            return Err(Error::IncompatibleOptions);
        }

        Ok(Box::new(InlineSignReady {
            inline_sign: *self,
            data,
        }))
    }
}

struct InlineSignReady<'a> {
    inline_sign: InlineSign,
    data: &'a mut (dyn io::Read + Send + Sync),
}

impl sop::ops::Ready for InlineSignReady<'_> {
    fn to_writer(self: Box<Self>, mut sink: &mut (dyn io::Write + Send + Sync)) -> sop::Result<()> {
        assert!(!self.inline_sign.sign.signers.is_empty()); // FIXME

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

        let mut data_signers: Vec<DataSigner> = vec![];
        for tsk in self.inline_sign.sign.signers {
            let mut s: Vec<DataSigner> = tsk.signing_capable_component_keys().collect();

            if s.is_empty() {
                panic!(
                    "no signing capable component key found for signer {:02x?}",
                    tsk.key().fingerprint()
                );
            }

            data_signers.append(&mut s);
        }

        if self.inline_sign.mode == sop::ops::InlineSignAs::ClearSigned {
            let mut data = vec![];
            self.data.read_to_end(&mut data)?;

            let signers = |text: &str| -> Result<Vec<Signature>, pgp::errors::Error> {
                let mut sigs: Vec<Signature> = vec![];

                for ds in data_signers {
                    let mut sig = None;

                    // try all passwords for this DataSigner
                    'pws: for pw in &pws {
                        let pw = pgp::types::Password::from(*pw);
                        let res =
                            ds.sign_data(text.as_bytes(), SignatureMode::Text, &pw, ds.hash_alg());

                        if let Ok(signature) = res {
                            sig = Some(signature);
                            break 'pws; // we found a working password for ds, stop trying more
                        }
                    }

                    if let Some(s) = sig {
                        sigs.push(s);
                    } else {
                        // We failed to produce a signature, for some reason.
                        // Maybe it's password protection.
                        // TODO: return a more concrete error?

                        return Err(pgp::errors::Error::Message {
                            message: "Couldn't sign".to_string(),
                            backtrace: None,
                        });
                    }
                }

                Ok(sigs)
            };

            let text = core::str::from_utf8(&data).map_err(|_| Error::BadData)?;
            let Ok(csf) = CleartextSignedMessage::new_many(text, signers) else {
                // The signers failed, for some reason. It might be password protection.
                // TODO: Check error conditions more closely, and return more specific errors.

                return Err(Error::KeyIsProtected);
            };

            csf.to_armored_writer(&mut sink, ArmorOptions::default())
                .expect("writing failed");

            Ok(())
        } else {
            let mut builder = MessageBuilder::from_reader("", self.data);

            if self.inline_sign.mode == sop::ops::InlineSignAs::Text {
                builder.sign_text();
            }

            for ds in &data_signers {
                // FIXME: password
                builder.sign(ds, pgp::types::Password::empty(), ds.hash_alg());
            }

            let all_v6 = !data_signers.iter().any(|s| s.version() != KeyVersion::V6);

            let rng = rand::thread_rng();
            if self.inline_sign.armor {
                let armor_options = ArmorOptions {
                    headers: None,
                    include_checksum: !all_v6,
                };

                builder
                    .to_armored_writer(rng, armor_options, sink)
                    .expect("write failure");
            } else {
                builder.to_writer(rng, sink).expect("write failure");
            }

            Ok(())
        }
    }
}
