use std::io::Cursor;

use anyhow::Result;
use gettextrs::gettext;
use serde::{de::Deserializer, Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
use zip::{self, ZipArchive};

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

#[allow(clippy::upper_case_acronyms)]
#[derive(Serialize, Deserialize)]
pub struct RaivoOTP;

/// A Raivo OTP entry.
///
/// [See Raivo's source code for each item's serialized form.][0]
///
/// [0]: https://github.com/raivo-otp/ios-application/blob/3a8aaa0ea16a761e6205abd2700ac90dd4c9c9b6/Raivo/Models/Password.swift#L104-L116
#[derive(Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct Item {
    #[zeroize(skip)]
    issuer: String,
    #[zeroize(skip)]
    account: String,
    secret: String,
    #[zeroize(skip)]
    algorithm: Algorithm,
    #[serde(deserialize_with = "deserialize_raivo_u32")]
    #[zeroize(skip)]
    digits: Option<u32>,
    #[serde(rename = "kind")]
    #[zeroize(skip)]
    method: Method,
    #[serde(rename = "timer")]
    #[serde(deserialize_with = "deserialize_raivo_u32")]
    #[zeroize(skip)]
    period: Option<u32>,
    #[serde(deserialize_with = "deserialize_raivo_u32")]
    #[zeroize(skip)]
    counter: Option<u32>,
}

fn deserialize_raivo_u32<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
where
    D: Deserializer<'de>,
{
    let n: u32 = String::deserialize(deserializer)?
        .parse()
        .map_err(serde::de::Error::custom)?;
    Ok(Some(n))
}

impl RestorableItem for Item {
    fn account(&self) -> String {
        self.account.clone()
    }

    fn issuer(&self) -> String {
        self.issuer.clone()
    }

    fn secret(&self) -> String {
        self.secret.clone()
    }

    fn period(&self) -> Option<u32> {
        match self.method() {
            Method::TOTP => self.period,
            Method::HOTP | Method::Steam => None,
        }
    }

    fn method(&self) -> Method {
        self.method
    }

    fn algorithm(&self) -> Algorithm {
        self.algorithm
    }

    fn digits(&self) -> Option<u32> {
        self.digits
    }

    fn counter(&self) -> Option<u32> {
        match self.method() {
            Method::HOTP => self.counter,
            Method::TOTP | Method::Steam => None,
        }
    }
}

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

    fn title() -> String {
        gettext("Raivo OTP")
    }

    fn subtitle() -> String {
        gettext("From a ZIP export generated by Raivo OTP")
    }

    /// Restore from a ZIP file generated by Raivo OTP.
    ///
    /// See Raivo's source code for [exporting the AES-encrypted ZIP][0].
    ///
    /// [0]: https://github.com/raivo-otp/ios-application/blob/3a8aaa0ea16a761e6205abd2700ac90dd4c9c9b6/Raivo/Features/DataExportFeature.swift#L188-L195
    fn restore_from_data(from: &[u8], key: Option<&str>) -> Result<Vec<Self::Item>> {
        let password: &[u8] = match key {
            None => &[],
            Some(k) => k.as_bytes(),
        };
        let mut archive = ZipArchive::new(Cursor::new(from))?;
        let file = archive.by_name_decrypt("raivo-otp-export.json", password)?;
        let items = serde_json::from_reader(file)?;
        Ok(items)
    }
}

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

    #[test]
    fn parse() {
        let data = std::fs::read("./src/backup/tests/raivootp.zip").unwrap();
        let items = RaivoOTP::restore_from_data(&data, Some("RaivoTest123")).unwrap();

        assert_eq!(items.len(), 2);

        assert_eq!(items[0].account(), "mason");
        assert_eq!(items[0].issuer(), "Example A");
        assert_eq!(items[0].secret(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789");
        assert_eq!(items[0].period(), Some(30));
        assert_eq!(items[0].method(), Method::TOTP);
        assert_eq!(items[0].algorithm(), Algorithm::SHA1);
        assert_eq!(items[0].digits(), Some(6));
        assert_eq!(items[0].counter(), None);

        assert_eq!(items[1].account(), "james");
        assert_eq!(items[1].issuer(), "Example B");
        assert_eq!(items[1].secret(), "12345678ABCDEFGHIJKLMNOPQRSTUVWXYZ");
        assert_eq!(items[1].period(), Some(123));
        assert_eq!(items[1].method(), Method::TOTP);
        assert_eq!(items[1].algorithm(), Algorithm::SHA256);
        assert_eq!(items[1].digits(), Some(8));
        assert_eq!(items[1].counter(), None);
    }

    #[test]
    fn invalid_zip() {
        let data: [u8; 3] = [1, 2, 3];
        assert!(RaivoOTP::restore_from_data(&data, Some("RaivoTest123")).is_err());
    }

    #[test]
    fn invalid_password() {
        let data = std::fs::read("./src/backup/tests/raivootp.zip").unwrap();
        assert!(RaivoOTP::restore_from_data(&data, Some("bad password")).is_err());
    }
}
