use openssl::{
    bn::BigNumContext,
    derive::Deriver,
    ec::{EcGroup, EcKey, EcPoint, PointConversionForm},
    nid::Nid,
    pkey::{Id, PKey, Private, Public},
};

use crate::{dh_compat::KeyAgreement, error::SecioError};

type PairsGenarate = fn() -> Result<(PKey<Private>, Vec<u8>), SecioError>;

struct Algorithm {
    _private_len: usize,
    pubkey_len: usize,
    pairs_generate: PairsGenarate,
    from_pubkey: fn(&[u8]) -> Result<PKey<Public>, SecioError>,
}

static ECDH_P256: Algorithm = Algorithm {
    _private_len: 256 / 8,
    pubkey_len: 1 + (2 * 256_usize.div_ceil(8)),
    pairs_generate: p256_generate,
    from_pubkey: p256_from_pubkey,
};

static ECDH_P384: Algorithm = Algorithm {
    _private_len: 384 / 8,
    pubkey_len: 1 + (2 * 384_usize.div_ceil(8)),
    pairs_generate: p384_generate,
    from_pubkey: p384_from_pubkey,
};

#[cfg(ossl110)]
static X25519: Algorithm = Algorithm {
    _private_len: 32,
    pubkey_len: 32,
    pairs_generate: x25519_generate,
    from_pubkey: x25519_from_pubkey,
};

impl From<KeyAgreement> for &'static Algorithm {
    fn from(src: KeyAgreement) -> &'static Algorithm {
        match src {
            KeyAgreement::EcdhP256 => &ECDH_P256,
            KeyAgreement::EcdhP384 => &ECDH_P384,
            #[cfg(ossl110)]
            KeyAgreement::X25519 => &X25519,
            #[cfg(not(ossl110))]
            _ => panic!("ECDH {:?} does not supported by openssl yet", src),
        }
    }
}

pub struct EphemeralPrivateKey {
    evp_key: PKey<Private>,
    al: &'static Algorithm,
}

fn ec_generate(nid: Nid) -> Result<(PKey<Private>, Vec<u8>), SecioError> {
    let group = EcGroup::from_curve_name(nid)?;

    let mut ctx = BigNumContext::new()?;

    let ec_key = EcKey::generate(&group)?;

    let pubkey =
        ec_key
            .public_key()
            .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)?;

    let evp_key = PKey::from_ec_key(ec_key)?;
    Ok((evp_key, pubkey))
}

fn ec_from_pubkey(nid: Nid, other_public_key: &[u8]) -> Result<PKey<Public>, SecioError> {
    let group = EcGroup::from_curve_name(nid)?;
    let mut ctx = BigNumContext::new()?;

    let point = EcPoint::from_bytes(&group, other_public_key, &mut ctx)?;

    let ec_key = EcKey::from_public_key(&group, &point)?;
    PKey::from_ec_key(ec_key).map_err(Into::into)
}

fn p256_generate() -> Result<(PKey<Private>, Vec<u8>), SecioError> {
    ec_generate(Nid::X9_62_PRIME256V1)
}

fn p256_from_pubkey(other_public_key: &[u8]) -> Result<PKey<Public>, SecioError> {
    ec_from_pubkey(Nid::X9_62_PRIME256V1, other_public_key)
}

fn p384_generate() -> Result<(PKey<Private>, Vec<u8>), SecioError> {
    ec_generate(Nid::SECP384R1)
}

fn p384_from_pubkey(other_public_key: &[u8]) -> Result<PKey<Public>, SecioError> {
    ec_from_pubkey(Nid::SECP384R1, other_public_key)
}

#[cfg(ossl110)]
fn x25519_generate() -> Result<(PKey<Private>, Vec<u8>), SecioError> {
    let evp_key = PKey::generate_x25519()?;

    let pubkey = evp_key.as_ref().raw_public_key()?;

    Ok((evp_key, pubkey))
}

#[cfg(ossl110)]
fn x25519_from_pubkey(pubkey: &[u8]) -> Result<PKey<Public>, SecioError> {
    PKey::public_key_from_raw_bytes(pubkey, Id::X25519).map_err(Into::into)
}

pub fn generate_agreement(
    algorithm: KeyAgreement,
) -> Result<(EphemeralPrivateKey, Vec<u8>), SecioError> {
    let al: &'static Algorithm = algorithm.into();

    let (evp_key, ecdh_public_key) = (al.pairs_generate)()?;

    Ok((EphemeralPrivateKey { evp_key, al }, ecdh_public_key))
}

pub fn agree(
    _algorithm: KeyAgreement,
    my_private_key: EphemeralPrivateKey,
    other_public_key: &[u8],
) -> Result<Vec<u8>, SecioError> {
    if other_public_key.len() != my_private_key.al.pubkey_len {
        return Err(SecioError::SecretGenerationFailed);
    }

    let peer_evp_key = (my_private_key.al.from_pubkey)(other_public_key)?;

    let mut deriver = Deriver::new(&my_private_key.evp_key)?;

    deriver.set_peer(&peer_evp_key)?;

    deriver.derive_to_vec().map_err(Into::into)
}

#[cfg(test)]
mod test {
    use super::{agree, generate_agreement};
    use crate::dh_compat::{
        KeyAgreement,
        ring_impl::{agree as agree_ring, generate_agreement as generate_agreement_ring},
    };

    fn test_ecdh(ty: KeyAgreement) {
        let (private_key, pub_key) = generate_agreement(ty).unwrap();

        let (peer_private_key, peer_pub_key) = generate_agreement_ring(ty).unwrap();

        let a = agree(ty, private_key, &peer_pub_key).unwrap();
        let b = agree_ring(ty, peer_private_key, &pub_key).unwrap();

        assert_eq!(a, b, "{:?} fail", ty)
    }

    #[test]
    fn test_all_ecdh() {
        test_ecdh(KeyAgreement::EcdhP256);
        test_ecdh(KeyAgreement::EcdhP384);
        #[cfg(ossl110)]
        test_ecdh(KeyAgreement::X25519);
    }
}
