use anyhow::Result;
use gettextrs::gettext;
use percent_encoding::percent_decode;
use prost::{Enumeration, Message};
use url::Url;

use super::Restorable;
use crate::models::{Algorithm, Method, OTPUri};

pub struct Google;

impl Restorable for Google {
    const ENCRYPTABLE: bool = false;
    const SCANNABLE: bool = true;
    const IDENTIFIER: &'static str = "google";
    type Item = OTPUri;

    fn title() -> String {
        gettext("Google Authenticator")
    }

    fn subtitle() -> String {
        gettext("From a QR code generated by Google Authenticator")
    }

    fn restore_from_data(from: &[u8], _key: Option<&str>) -> Result<Vec<Self::Item>> {
        let string = String::from_utf8(from.into())?;
        let uri = Url::parse(&string)?;

        if uri.scheme() != "otpauth-migration" {
            anyhow::bail!("Invalid OTP migration uri format, expected uri protocol to be otpauth-migration, got {}", uri.scheme());
        }

        if let Some(host) = uri.host_str() {
            if host != "offline" {
                anyhow::bail!(
                    "Invalid OTP migration uri format, expected uri host to be offline, got {host}"
                );
            }
        } else {
            anyhow::bail!(
                "Invalid OTP migration uri format, expected uri host to be offline, got nothing"
            );
        }

        let data = uri.query_pairs().fold(None, |folded, (key, value)| {
            folded.or_else(|| match key.into_owned().as_str() {
                "data" => {
                    let bytes = value.into_owned().into_bytes();
                    let decoded = percent_decode(&bytes);
                    let decoded = match data_encoding::BASE64.decode(&decoded.collect::<Vec<u8>>())
                    {
                        Ok(decoded) => decoded,
                        Err(_) => return None,
                    };
                    Some(match protobuf::MigrationPayload::decode(&*decoded) {
                        Ok(decoded) => decoded,
                        Err(_) => return None,
                    })
                }
                _ => None,
            })
        });

        let data = if let Some(data) = data {
            data
        } else {
            anyhow::bail!("Invalid OTP migration uri format, expected a data query parameter");
        };

        let data_len = data.otp_parameters.len();

        let mut restored = data.otp_parameters.into_iter().fold(
            Vec::with_capacity(data_len),
            |mut folded, otp| {
                folded.push(OTPUri {
                    algorithm: match otp.algorithm() {
                        protobuf::migration_payload::Algorithm::ALGO_INVALID => return folded,
                        protobuf::migration_payload::Algorithm::ALGO_SHA1 => Algorithm::SHA1,
                    },
                    digits: match otp.r#type() {
                        protobuf::migration_payload::OtpType::OTP_HOTP => Some(otp.digits as u32),
                        _ => None,
                    },
                    method: match otp.r#type() {
                        protobuf::migration_payload::OtpType::OTP_INVALID => return folded,
                        protobuf::migration_payload::OtpType::OTP_HOTP => Method::HOTP,
                        protobuf::migration_payload::OtpType::OTP_TOTP => Method::TOTP,
                    },
                    secret: {
                        let string = data_encoding::BASE32_NOPAD.encode(&otp.secret);

                        string.trim_end_matches(['\0', '=']).to_owned()
                    },
                    label: otp.name.clone(),
                    issuer: otp.issuer.clone(),
                    period: None,
                    counter: Some(otp.counter as u32),
                });
                folded
            },
        );

        restored.shrink_to_fit();

        Ok(restored)
    }
}

#[allow(non_camel_case_types)]
mod protobuf {
    use super::*;

    #[derive(Message)]
    pub struct MigrationPayload {
        #[prost(message, repeated)]
        pub otp_parameters: Vec<migration_payload::OtpParameters>,
        #[prost(int32)]
        pub version: i32,
        #[prost(int32)]
        pub batch_size: i32,
        #[prost(int32)]
        pub batch_index: i32,
        #[prost(int32)]
        pub batch_id: i32,
    }

    pub mod migration_payload {
        use zeroize::{Zeroize, ZeroizeOnDrop};

        use super::*;

        #[derive(Debug, Enumeration)]
        pub enum Algorithm {
            ALGO_INVALID = 0,
            ALGO_SHA1 = 1,
        }

        #[derive(Debug, Enumeration)]
        pub enum OtpType {
            OTP_INVALID = 0,
            OTP_HOTP = 1,
            OTP_TOTP = 2,
        }

        #[derive(Message, Zeroize, ZeroizeOnDrop)]
        pub struct OtpParameters {
            #[prost(bytes)]
            pub secret: Vec<u8>,
            #[zeroize(skip)]
            #[prost(string)]
            pub name: String,
            #[prost(string)]
            #[zeroize(skip)]
            pub issuer: String,
            #[prost(enumeration = "Algorithm")]
            #[zeroize(skip)]
            pub algorithm: i32,
            #[prost(int32)]
            #[zeroize(skip)]
            pub digits: i32,
            #[prost(enumeration = "OtpType")]
            #[zeroize(skip)]
            pub r#type: i32,
            #[prost(int64)]
            #[zeroize(skip)]
            pub counter: i64,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{super::RestorableItem, *};

    #[test]
    fn parse() {
        let data = b"otpauth-migration://offline?data=CjYKEExyJfPiZeroMa/MdF%2BnkTISE2pvaG5kb2VAZXhhbXBsZS5jb20aB0Rpc2NvcmQgASgBMAIQARgBIAA%3D";
        let items = Google::restore_from_data(data, None).unwrap();

        assert_eq!(items[0].account(), "johndoe@example.com");
        assert_eq!(items[0].issuer(), "Discord");
        assert_eq!(items[0].secret(), "JRZCL47CMXVOQMNPZR2F7J4RGI");
        assert_eq!(items[0].period(), None);
        assert_eq!(items[0].algorithm(), Algorithm::SHA1);
        assert_eq!(items[0].digits(), None);
        assert_eq!(items[0].counter(), Some(0));
    }
}
