use challenge_response::{
    config::{Config, Mode, Slot},
    ChallengeResponse,
};
use cipher::InvalidLength;
use hex::FromHexError;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};

use crate::key::KeyElement;

#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)]
#[non_exhaustive]
pub enum ChallengeResponseKey {
    LocalChallenge(String),
    YubikeyChallenge(Yubikey, String),
}

#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)]
pub struct Yubikey {
    pub serial_number: u32,
    pub name: Option<String>,
}

impl ChallengeResponseKey {
    pub(crate) fn perform_challenge(&self, challenge: &[u8]) -> Result<KeyElement, ChallengeResponseKeyError> {
        match self {
            ChallengeResponseKey::LocalChallenge(secret) => {
                let secret_bytes = hex::decode(secret)?;
                let response = crate::crypt::calculate_hmac_sha1(&[challenge], &secret_bytes)?.to_vec();
                Ok(response)
            }
            ChallengeResponseKey::YubikeyChallenge(yubikey, slot_number) => {
                let mut challenge_response_client = ChallengeResponse::new()?;
                let slot = Slot::from_str(slot_number)
                    .ok_or(ChallengeResponseKeyError::InvalidSlot(slot_number.to_string()))?;

                let device = challenge_response_client.find_device_from_serial(yubikey.serial_number)?;

                let mut config = Config::new_from(device);
                config = config.set_variable_size(true);
                config = config.set_mode(Mode::Sha1);
                config = config.set_slot(slot);

                let key_element = challenge_response_client
                    .challenge_response_hmac(challenge, config)?
                    .to_vec();

                Ok(key_element)
            }
        }
    }

    pub fn get_available_yubikeys() -> Result<Vec<Yubikey>, ChallengeResponseKeyError> {
        let mut challenge_response_client = ChallengeResponse::new()?;

        let devices = challenge_response_client
            .find_all_devices()?
            .into_iter()
            .filter_map(|device| {
                let serial_number = device.serial?;
                let name = device.name;

                Some(Yubikey { serial_number, name })
            })
            .collect();

        Ok(devices)
    }

    pub fn get_yubikey(serial_number: Option<u32>) -> Result<Yubikey, ChallengeResponseKeyError> {
        let devices = ChallengeResponseKey::get_available_yubikeys()?;
        if devices.is_empty() {
            return Err(ChallengeResponseKeyError::NoKeys);
        }

        if let Some(serial_number) = serial_number {
            let key = devices
                .iter()
                .find(|y| y.serial_number == serial_number)
                .ok_or(ChallengeResponseKeyError::KeyNotFound(serial_number))?;

            Ok(key.clone())
        } else {
            if devices.len() > 1 {
                return Err(ChallengeResponseKeyError::AmbiguousKeys);
            }
            Ok(devices[0].clone())
        }
    }
}

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ChallengeResponseKeyError {
    #[error("No challenge-respone keys are connected to the system.")]
    NoKeys,

    #[error("Multiple challenge-response keys are connected to the system. Please provide a serial number.")]
    AmbiguousKeys,

    #[error("Challenge-response key with serial number {0} not found.")]
    KeyNotFound(u32),

    #[error("Invalid key slot: {0}")]
    InvalidSlot(String),

    #[error(transparent)]
    Api(#[from] challenge_response::error::ChallengeResponseError),

    #[error("Error decoding local challenge secret: {0}")]
    Hex(#[from] FromHexError),

    #[error("Local secret has invalid length")]
    InvalidLength(#[from] InvalidLength),

    #[error("Challenge-response authentication was not performed")]
    NotPerformed,
}
