use blake2::{
    Blake2bVar,
    digest::{Update, VariableOutput},
};
use bytes::{BufMut, Bytes, BytesMut};
use chacha20poly1305::{
    XChaCha20Poly1305, XNonce,
    aead::{Aead, AeadCore, KeyInit, OsRng},
};
use qrcodegen::QrCode;
use x25519_dalek::{PublicKey, StaticSecret};

use totp_rs::{Algorithm, Secret, TOTP};

use std::error::Error;
use std::fmt::Write;

pub struct AuthClient {}

impl AuthClient {
    /// `account_len.account.client_start`
    /// (`client_state`, payload)
    pub fn registration_start_req(
        account: &[u8],
        password: &[u8],
        invite_token: Option<Bytes>,
    ) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
        let (client_state, client_start) = crate::registration::client_start(password)?;

        let account_len = account.len();
        if account_len > crate::MAX_ACCOUNT_LEN as usize {
            return Err("account is too long".into());
        }

        let mut buf = BytesMut::with_capacity(1 + account_len + 32);

        buf.put_u8(u8::try_from(account_len)?);
        buf.put(account);
        buf.put(&client_start[..]);

        if let Some(token) = invite_token {
            buf.put(token);
        }

        Ok((client_state, buf.into()))
    }

    /// `account_len.account.client_finish`
    /// payload
    pub fn registration_finish_req(
        account: &[u8],
        password: &[u8],
        client_state: &[u8],
        server_message: &[u8],
    ) -> Result<([u8; 32], Bytes), Box<dyn Error>> {
        let client_finish =
            crate::registration::client_finish(password, client_state, server_message)?;

        let account_len = account.len();
        if account_len > crate::MAX_ACCOUNT_LEN as usize {
            return Err("account is too long".into());
        }

        let static_secret = StaticSecret::random_from_rng(OsRng);
        let public_key = PublicKey::from(&static_secret);

        let private_key = *static_secret.as_bytes();

        let mut buf = BytesMut::with_capacity(1 + account_len + client_finish.len());

        buf.put_u8(u8::try_from(account_len)?);
        buf.put(account);
        buf.put(&public_key.as_bytes()[..]);
        buf.put(&client_finish[..]);

        Ok((private_key, buf.into()))
    }

    pub fn decrypt_totp_mfa(
        response: &Bytes,
        private_key: [u8; 32],
        app_name: String,
        account: String,
    ) -> Result<(TOTP, String), Box<dyn Error>> {
        let static_secret = StaticSecret::from(private_key);
        let public_key_bytes: [u8; 32] = response[115..147].try_into()?;
        let public_key = PublicKey::from(public_key_bytes);

        let shared_secret = static_secret.diffie_hellman(&public_key);

        let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());

        let nonce = XNonce::from_slice(&response[91..115]);
        let (secret, codes) = match cipher.decrypt(nonce, &response[0..91]) {
            Ok(secret_and_codes) => (
                secret_and_codes[0..20].to_vec(),
                std::str::from_utf8(&secret_and_codes[20..])?.to_string(),
            ),
            Err(err) => return Err(err.to_string().into()),
        };

        let totp = TOTP::new(
            Algorithm::SHA1,
            6,
            1,
            30,
            Secret::Raw(secret).to_bytes()?,
            Some(app_name),
            account,
        )?;

        Ok((totp, codes))
    }

    pub fn decrypt_totp_mfa_code(
        response: &Bytes,
        private_key: [u8; 32],
        app_name: String,
        account: String,
    ) -> Result<(String, String), Box<dyn Error>> {
        let (totp, recovery_codes) =
            Self::decrypt_totp_mfa(response, private_key, app_name, account)?;
        let mfa_code = totp.generate_current()?;
        Ok((mfa_code, recovery_codes))
    }

    pub fn decrypt_totp_mfa_to_qr_svg(
        response: &Bytes,
        private_key: [u8; 32],
        app_name: String,
        account: String,
    ) -> Result<(String, String), Box<dyn Error>> {
        let (totp, recovery_codes) =
            Self::decrypt_totp_mfa(response, private_key, app_name, account)?;
        let qr: QrCode = QrCode::encode_text(&totp.get_url(), qrcodegen::QrCodeEcc::Medium)?;

        let svg = to_svg_string(&qr, 4)?;
        Ok((svg, recovery_codes))
    }

    /// `account_len.account.client_start`
    /// (`client_state`, payload)
    pub fn login_start_req(
        account: &[u8],
        password: &[u8],
    ) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
        let (client_state, client_start) = crate::login::client_start(password)?;

        let account_len = account.len();
        if account_len > crate::MAX_ACCOUNT_LEN as usize {
            return Err("account is too long".into());
        }

        let mut buf = BytesMut::with_capacity(1 + account_len + client_start.len());

        buf.put_u8(u8::try_from(account_len)?);
        buf.put(account);
        buf.put(&client_start[..]);

        Ok((client_state, buf.into()))
    }

    /// `account_len.account.client_finish`
    /// payload
    pub fn login_finish_req(
        account: &[u8],
        password: &[u8],
        mfa_hash: &[u8],
        client_state: &[u8],
        server_message: &[u8],
        client_verifier: Option<&[u8]>,
    ) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
        let (client_finish, session_key) =
            crate::login::client_finish(password, client_state, server_message)?;

        let account_len = account.len();
        if account_len > crate::MAX_ACCOUNT_LEN as usize {
            return Err("account is too long".into());
        }

        let mut buf = BytesMut::with_capacity(1 + account_len + 32 + 32 + 16 + 24 + 64);

        buf.put_u8(u8::try_from(account_len)?);
        buf.put(account);

        let mut key = [0u8; 32];

        let mut hasher = match Blake2bVar::new(32) {
            Ok(h) => h,
            Err(err) => return Err(err.to_string().into()),
        };
        hasher.update(&session_key[..]);
        if let Err(err) = hasher.finalize_variable(&mut key) {
            return Err(err.to_string().into());
        }

        let cipher = XChaCha20Poly1305::new(&key.into());

        let mut mfa_hash_verifier_key = BytesMut::with_capacity(64);
        mfa_hash_verifier_key.put(mfa_hash);

        if let Some(client_verifier) = client_verifier {
            mfa_hash_verifier_key.put(client_verifier);
        }

        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
        let mut encrypted_mfa_hash = match cipher.encrypt(&nonce, &mfa_hash_verifier_key[..]) {
            Ok(e) => e,
            Err(err) => return Err(err.to_string().into()),
        };
        encrypted_mfa_hash.extend_from_slice(&nonce);

        buf.put(&encrypted_mfa_hash[..]);
        buf.put(&client_finish[..]);

        Ok((buf.into(), session_key))
    }

    pub fn decrypt_token(response: &Bytes, session_key: &[u8]) -> Result<Bytes, Box<dyn Error>> {
        crate::login::decrypt_token(response, session_key)
    }

    pub fn reset_password_login_start_req(
        account: &[u8],
        password: &[u8],
    ) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
        Self::login_start_req(account, password)
    }

    pub fn reset_password_login_finish_req(
        account: &[u8],
        password: &[u8],
        mfa_hash: &[u8],
        client_state: &[u8],
        server_message: &[u8],
        client_verifier: Option<&[u8]>,
    ) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
        Self::login_finish_req(
            account,
            password,
            mfa_hash,
            client_state,
            server_message,
            client_verifier,
        )
    }

    pub fn password_reset_registration_start_req(
        account: &[u8],
        password: &[u8],
    ) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
        Self::registration_start_req(account, password, None)
    }

    pub fn password_reset_registration_finish_req(
        account: &[u8],
        password: &[u8],
        client_state: &[u8],
        server_message: &[u8],
    ) -> Result<Bytes, Box<dyn Error>> {
        let (_, req) =
            Self::registration_finish_req(account, password, client_state, server_message)?;

        Ok(req)
    }

    pub fn forgot_password_start_req(
        account: &[u8],
        new_password: &[u8],
        recovery_code: &[u8],
    ) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
        let (state, payload) = Self::registration_start_req(account, new_password, None)?;

        let mut payload: BytesMut = payload.into();
        payload.extend_from_slice(recovery_code);

        Ok((state, payload.into()))
    }

    pub fn forgot_password_finish_req(
        account: &[u8],
        new_password: &[u8],
        client_state: &[u8],
        server_message: &[u8],
        recovery_code: &[u8],
    ) -> Result<Bytes, Box<dyn Error>> {
        let (_, payload) =
            Self::registration_finish_req(account, new_password, client_state, server_message)?;

        let mut payload: BytesMut = payload.into();
        payload.extend_from_slice(recovery_code);

        Ok(payload.into())
    }

    pub fn reset_totp_mfa_start_req(
        account: &[u8],
        password: &[u8],
    ) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
        Self::login_start_req(account, password)
    }

    pub fn reset_totp_mfa_finish_req(
        account: &[u8],
        password: &[u8],
        mfa_hash: &[u8],
        client_state: &[u8],
        server_message: &[u8],
    ) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
        Self::login_finish_req(
            account,
            password,
            mfa_hash,
            client_state,
            server_message,
            None,
        )
    }

    pub fn decrypt_reset_totp_mfa(
        response: &Bytes,
        session_key: &[u8],
    ) -> Result<Bytes, Box<dyn Error>> {
        crate::login::decrypt_token(response, session_key)
    }

    pub fn decrypt_reset_totp_mfa_code(
        response: &Bytes,
        session_key: &[u8],
        app_name: String,
        account: String,
    ) -> Result<String, Box<dyn Error>> {
        let secret = Self::decrypt_reset_totp_mfa(response, session_key)?;

        let totp = TOTP::new(
            Algorithm::SHA1,
            6,
            1,
            30,
            Secret::Raw(secret.to_vec()).to_bytes()?,
            Some(app_name),
            account,
        )?;

        let mfa_code = totp.generate_current()?;

        Ok(mfa_code)
    }

    pub fn decrypt_reset_totp_mfa_to_qr_svg(
        response: &Bytes,
        session_key: &[u8],
        app_name: String,
        account: String,
    ) -> Result<String, Box<dyn Error>> {
        let secret = Self::decrypt_reset_totp_mfa(response, session_key)?;

        let totp = TOTP::new(
            Algorithm::SHA1,
            6,
            1,
            30,
            Secret::Raw(secret.to_vec()).to_bytes()?,
            Some(app_name),
            account,
        )?;

        let qr: QrCode = QrCode::encode_text(&totp.get_url(), qrcodegen::QrCodeEcc::Medium)?;
        let svg = to_svg_string(&qr, 4)?;

        Ok(svg)
    }

    pub fn lost_totp_mfa_start_req(
        account: &[u8],
        password: &[u8],
        recovery_code: &[u8],
    ) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
        let (state, payload) = Self::login_start_req(account, password)?;

        let mut payload: BytesMut = payload.into();
        payload.extend_from_slice(recovery_code);

        Ok((state, payload.into()))
    }

    pub fn lost_totp_mfa_finish_req(
        account: &[u8],
        password: &[u8],
        client_state: &[u8],
        server_message: &[u8],
        recovery_code: &[u8],
    ) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
        let (payload, session_key) = Self::login_finish_req(
            account,
            password,
            &[0u8; 32][..],
            client_state,
            server_message,
            None,
        )?;

        let mut payload: BytesMut = payload.into();
        payload.extend_from_slice(recovery_code);

        Ok((payload.into(), session_key))
    }

    pub fn decrypt_lost_totp_mfa(
        response: &Bytes,
        session_key: &[u8],
    ) -> Result<Bytes, Box<dyn Error>> {
        Self::decrypt_reset_totp_mfa(response, session_key)
    }

    pub fn decrypt_lost_totp_mfa_code(
        response: &Bytes,
        session_key: &[u8],
        app_name: String,
        account: String,
    ) -> Result<String, Box<dyn Error>> {
        Self::decrypt_reset_totp_mfa_code(response, session_key, app_name, account)
    }

    pub fn decrypt_lost_totp_mfa_to_qr_svg(
        response: &Bytes,
        session_key: &[u8],
        app_name: String,
        account: String,
    ) -> Result<String, Box<dyn Error>> {
        Self::decrypt_reset_totp_mfa_to_qr_svg(response, session_key, app_name, account)
    }

    pub fn reset_recovery_codes_start_req(
        account: &[u8],
        password: &[u8],
    ) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
        Self::login_start_req(account, password)
    }

    pub fn reset_recovery_codes_finish_req(
        account: &[u8],
        password: &[u8],
        mfa_hash: &[u8],
        client_state: &[u8],
        server_message: &[u8],
    ) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
        Self::login_finish_req(
            account,
            password,
            mfa_hash,
            client_state,
            server_message,
            None,
        )
    }

    pub fn decrypt_reset_recovery_codes(
        response: &Bytes,
        session_key: &[u8],
    ) -> Result<String, Box<dyn Error>> {
        let codes = crate::login::decrypt_token(response, session_key)?;
        let codes_str = std::str::from_utf8(&codes)?.to_string();

        Ok(codes_str)
    }

    /// `refresh_token`
    #[must_use]
    pub fn access_get_req(refresh_token: &[u8]) -> Bytes {
        Bytes::copy_from_slice(refresh_token)
    }

    pub fn delete_account_start_req(
        account: &[u8],
        password: &[u8],
    ) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
        Self::login_start_req(account, password)
    }

    pub fn delete_account_finish_req(
        account: &[u8],
        password: &[u8],
        mfa_hash: &[u8],
        client_state: &[u8],
        server_message: &[u8],
    ) -> Result<Bytes, Box<dyn Error>> {
        let (req, _) = Self::login_finish_req(
            account,
            password,
            mfa_hash,
            client_state,
            server_message,
            None,
        )?;

        Ok(req)
    }
}

// copied from qrcodegen examples https://github.com/nayuki/QR-Code-generator/blob/2c9044de6b049ca25cb3cd1649ed7e27aa055138/rust/examples/qrcodegen-demo.rs#L169
fn to_svg_string(qr: &QrCode, border: u8) -> Result<String, Box<dyn Error>> {
    let border: i32 = border.into();
    let mut result = String::new();
    // result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    // result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";

    if let Some(border) = border.checked_mul(2)
        && let Some(dimension) = qr.size().checked_add(border)
    {
        writeln!(
            result,
            "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {dimension} {dimension}\" stroke=\"none\">"
        )?;
        result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
        result += "\t<path d=\"";
        for y in 0..qr.size() {
            for x in 0..qr.size() {
                if qr.get_module(x, y) {
                    if x != 0 || y != 0 {
                        result += " ";
                    }
                    write!(result, "M{},{}h1v1h-1z", x + border, y + border)?;
                }
            }
        }
        result += "\" fill=\"#000000\"/>\n";
        result += "</svg>\n";
    }

    Ok(result)
}
