//! Manage Nebula PKI Certificates
//! This is pretty much a direct port of nebula/cert/cert.go

use std::error::Error;
use std::fmt::{Display, Formatter};
use std::net::Ipv4Addr;
use std::ops::Add;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use ipnet::{Ipv4Net};
use pem::Pem;
use quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer};
use sha2::Sha256;
use crate::ca::NebulaCAPool;
use crate::cert_codec::{RawNebulaCertificate, RawNebulaCertificateDetails};
use sha2::Digest;

/// The length, in bytes, of public keys
pub const PUBLIC_KEY_LENGTH: i32 = 32;

/// The PEM banner for certificates
pub const CERT_BANNER: &str = "NEBULA CERTIFICATE";
/// The PEM banner for X25519 private keys
pub const X25519_PRIVATE_KEY_BANNER: &str = "NEBULA X25519 PRIVATE KEY";
/// The PEM banner for X25519 public keys
pub const X25519_PUBLIC_KEY_BANNER: &str = "NEBULA X25519 PUBLIC KEY";
/// The PEM banner for Ed25519 private keys
pub const ED25519_PRIVATE_KEY_BANNER: &str = "NEBULA ED25519 PRIVATE KEY";
/// The PEM banner for Ed25519 public keys
pub const ED25519_PUBLIC_KEY_BANNER: &str = "NEBULA ED25519 PUBLIC KEY";

/// A Nebula PKI certificate
#[derive(Debug, Clone)]
pub struct NebulaCertificate {
    /// The signed data of this certificate
    pub details: NebulaCertificateDetails,
    /// The Ed25519 signature of this certificate
    pub signature: Vec<u8>
}

/// The signed details contained in a Nebula PKI certificate
#[derive(Debug, Clone)]
pub struct NebulaCertificateDetails {
    /// The name of the identity this certificate was issued for
    pub name: String,
    /// The IPv4 addresses issued to this node
    pub ips: Vec<Ipv4Net>,
    /// The IPv4 subnets this node is responsible for routing
    pub subnets: Vec<Ipv4Net>,
    /// The groups this node is a part of
    pub groups: Vec<String>,
    /// Certificate start date and time
    pub not_before: SystemTime,
    /// Certificate expiry date and time
    pub not_after: SystemTime,
    /// Public key
    pub public_key: [u8; PUBLIC_KEY_LENGTH as usize],
    /// Is this node a CA?
    pub is_ca: bool,
    /// SHA256 of issuer certificate. If blank, this cert is self-signed.
    pub issuer: String
}

/// A list of errors that can occur parsing certificates
#[derive(Debug)]
pub enum CertificateError {
    /// Attempted to deserialize a certificate from an empty byte array
    EmptyByteArray,
    /// The encoded Details field is null
    NilDetails,
    /// The encoded Ips field is not formatted correctly
    IpsNotPairs,
    /// The encoded Subnets field is not formatted correctly
    SubnetsNotPairs,
    /// Signatures are expected to be 64 bytes but the signature on the certificate was a different length
    WrongSigLength,
    /// Public keys are expected to be 32 bytes but the public key on this cert is not
    WrongKeyLength,
    /// Certificates should have the PEM tag `NEBULA CERTIFICATE`, but this block did not
    WrongPemTag,
    /// This certificate either is not yet valid or has already expired
    Expired,
    /// The public key does not match the expected value
    KeyMismatch
}
#[cfg(not(tarpaulin_include))]
impl Display for CertificateError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::EmptyByteArray => write!(f, "Certificate bytearray is empty"),
            Self::NilDetails => write!(f, "The encoded Details field is null"),
            Self::IpsNotPairs => write!(f, "encoded IPs should be in pairs, an odd number was found"),
            Self::SubnetsNotPairs => write!(f, "encoded subnets should be in pairs, an odd number was found"),
            Self::WrongSigLength => write!(f, "Signature should be 64 bytes but is a different size"),
            Self::WrongKeyLength => write!(f, "Public keys are expected to be 32 bytes but the public key on this cert is not"),
            Self::WrongPemTag => write!(f, "Certificates should have the PEM tag `NEBULA CERTIFICATE`, but this block did not"),
            Self::Expired => write!(f, "This certificate either is not yet valid or has already expired"),
            Self::KeyMismatch => write!(f, "Key does not match expected value")
        }
    }
}
impl Error for CertificateError {}

fn map_cidr_pairs(pairs: &[u32]) -> Result<Vec<Ipv4Net>, Box<dyn Error>> {
    let mut res_vec = vec![];
    for pair in pairs.chunks(2) {
        res_vec.push(Ipv4Net::with_netmask(Ipv4Addr::from(pair[0]), Ipv4Addr::from(pair[1]))?);
    }
    Ok(res_vec)
}

#[cfg(not(tarpaulin_include))]
impl Display for NebulaCertificate {
    #[allow(clippy::unwrap_used)]
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "NebulaCertificate {{")?;
        writeln!(f, "   Details {{")?;
        writeln!(f, "       Name: {}", self.details.name)?;
        writeln!(f, "       Ips: {:?}", self.details.ips)?;
        writeln!(f, "       Subnets: {:?}", self.details.subnets)?;
        writeln!(f, "       Groups: {:?}", self.details.groups)?;
        writeln!(f, "       Not before: {:?}", self.details.not_before)?;
        writeln!(f, "       Not after: {:?}", self.details.not_after)?;
        writeln!(f, "       Is CA: {}", self.details.is_ca)?;
        writeln!(f, "       Issuer: {}", self.details.issuer)?;
        writeln!(f, "       Public key: {}", hex::encode(self.details.public_key))?;
        writeln!(f, "   }}")?;
        writeln!(f, "   Fingerprint: {}", self.sha256sum().unwrap())?;
        writeln!(f, "   Signature: {}", hex::encode(self.signature.clone()))?;
        writeln!(f, "}}")
    }
}

/// Given a protobuf-encoded certificate bytearray, deserialize it into a `NebulaCertificate` object.
/// # Errors
/// This function will return an error if there is a protobuf parsing error, or if the certificate data is invalid.
/// # Panics
pub fn deserialize_nebula_certificate(bytes: &[u8]) -> Result<NebulaCertificate, Box<dyn Error>> {
    if bytes.is_empty() {
        return Err(CertificateError::EmptyByteArray.into())
    }

    let mut reader = BytesReader::from_bytes(bytes);

    let raw_cert = RawNebulaCertificate::from_reader(&mut reader, bytes)?;

    let details = raw_cert.Details.ok_or(CertificateError::NilDetails)?;

    if details.Ips.len() % 2 != 0 {
        return Err(CertificateError::IpsNotPairs.into())
    }

    if details.Subnets.len() % 2 != 0 {
        return Err(CertificateError::SubnetsNotPairs.into())
    }

    let mut nebula_cert;
    #[allow(clippy::cast_sign_loss)]
    {
        nebula_cert = NebulaCertificate {
            details: NebulaCertificateDetails {
                name: details.Name.to_string(),
                ips: map_cidr_pairs(&details.Ips)?,
                subnets: map_cidr_pairs(&details.Subnets)?,
                groups: details.Groups.iter().map(std::string::ToString::to_string).collect(),
                not_before: SystemTime::UNIX_EPOCH.add(Duration::from_secs(details.NotBefore as u64)),
                not_after: SystemTime::UNIX_EPOCH.add(Duration::from_secs(details.NotAfter as u64)),
                public_key: [0u8; 32],
                is_ca: details.IsCA,
                issuer: hex::encode(details.Issuer),
            },
            signature: vec![],
        };
    }

    nebula_cert.signature = raw_cert.Signature;

    if details.PublicKey.len() != 32 {
        return Err(CertificateError::WrongKeyLength.into())
    }

    #[allow(clippy::unwrap_used)] { nebula_cert.details.public_key = details.PublicKey.try_into().unwrap(); }

    Ok(nebula_cert)
}

/// A list of errors that can occur parsing keys
#[derive(Debug)]
pub enum KeyError {
    /// Keys should have their associated PEM tags but this had the wrong one
    WrongPemTag,
    /// Ed25519 private keys are 64 bytes
    Not64Bytes,
    /// X25519 private keys are 32 bytes
    Not32Bytes
}
#[cfg(not(tarpaulin_include))]
impl Display for KeyError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::WrongPemTag => write!(f, "Keys should have their associated PEM tags but this had the wrong one"),
            Self::Not64Bytes => write!(f, "Ed25519 private keys are 64 bytes"),
            Self::Not32Bytes => write!(f, "X25519 private keys are 32 bytes")
        }
    }
}
impl Error for KeyError {}


/// Deserialize the first PEM block in the given byte array into a `NebulaCertificate`
/// # Errors
/// This function will return an error if the PEM data is invalid, or if there is an error parsing the certificate (see `deserialize_nebula_certificate`)
pub fn deserialize_nebula_certificate_from_pem(bytes: &[u8]) -> Result<NebulaCertificate, Box<dyn Error>> {
    let pem = pem::parse(bytes)?;
    if pem.tag != CERT_BANNER {
        return Err(CertificateError::WrongPemTag.into())
    }
    deserialize_nebula_certificate(&pem.contents)
}

/// Simple helper to PEM encode an X25519 private key
pub fn serialize_x25519_private(bytes: &[u8]) -> Vec<u8> {
    pem::encode(&Pem {
        tag: X25519_PRIVATE_KEY_BANNER.to_string(),
        contents: bytes.to_vec(),
    }).as_bytes().to_vec()
}

/// Simple helper to PEM encode an X25519 public key
pub fn serialize_x25519_public(bytes: &[u8]) -> Vec<u8> {
    pem::encode(&Pem {
        tag: X25519_PUBLIC_KEY_BANNER.to_string(),
        contents: bytes.to_vec(),
    }).as_bytes().to_vec()
}

/// Attempt to deserialize a PEM encoded X25519 private key
/// # Errors
/// This function will return an error if the PEM data is invalid or has the wrong tag
pub fn deserialize_x25519_private(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
    let pem = pem::parse(bytes)?;
    if pem.tag != X25519_PRIVATE_KEY_BANNER {
        return Err(KeyError::WrongPemTag.into())
    }
    if pem.contents.len() != 32 {
        return Err(KeyError::Not32Bytes.into())
    }
    Ok(pem.contents)
}

/// Attempt to deserialize a PEM encoded X25519 public key
/// # Errors
/// This function will return an error if the PEM data is invalid or has the wrong tag
pub fn deserialize_x25519_public(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
    let pem = pem::parse(bytes)?;
    if pem.tag != X25519_PUBLIC_KEY_BANNER {
        return Err(KeyError::WrongPemTag.into())
    }
    if pem.contents.len() != 32 {
        return Err(KeyError::Not32Bytes.into())
    }
    Ok(pem.contents)
}

/// Simple helper to PEM encode an Ed25519 private key
pub fn serialize_ed25519_private(bytes: &[u8]) -> Vec<u8> {
    pem::encode(&Pem {
        tag: ED25519_PRIVATE_KEY_BANNER.to_string(),
        contents: bytes.to_vec(),
    }).as_bytes().to_vec()
}

/// Simple helper to PEM encode an Ed25519 public key
pub fn serialize_ed25519_public(bytes: &[u8]) -> Vec<u8> {
    pem::encode(&Pem {
        tag: ED25519_PUBLIC_KEY_BANNER.to_string(),
        contents: bytes.to_vec(),
    }).as_bytes().to_vec()
}

/// Attempt to deserialize a PEM encoded Ed25519 private key
/// # Errors
/// This function will return an error if the PEM data is invalid or has the wrong tag
pub fn deserialize_ed25519_private(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
    let pem = pem::parse(bytes)?;
    if pem.tag != ED25519_PRIVATE_KEY_BANNER {
        return Err(KeyError::WrongPemTag.into())
    }
    if pem.contents.len() != 64 {
        return Err(KeyError::Not64Bytes.into())
    }
    Ok(pem.contents)
}

/// Attempt to deserialize a PEM encoded Ed25519 public key
/// # Errors
/// This function will return an error if the PEM data is invalid or has the wrong tag
pub fn deserialize_ed25519_public(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
    let pem = pem::parse(bytes)?;
    if pem.tag != ED25519_PUBLIC_KEY_BANNER {
        return Err(KeyError::WrongPemTag.into())
    }
    if pem.contents.len() != 64 {
        return Err(KeyError::Not64Bytes.into())
    }
    Ok(pem.contents)
}

impl NebulaCertificate {
    /// Sign a nebula certificate with the provided private key
    /// # Errors
    /// This function will return an error if the certificate could not be serialized or signed.
    pub fn sign(&mut self, key: &SigningKey) -> Result<(), Box<dyn Error>> {
        let mut out = Vec::new();
        let mut writer = Writer::new(&mut out);
        self.get_raw_details().write_message(&mut writer)?;

        self.signature = key.sign(&out).to_vec();
        Ok(())
    }

    /// Verify the signature on a certificate with the provided public key
    /// # Errors
    /// This function will return an error if the certificate could not be serialized or the signature could not be checked.
    pub fn check_signature(&self, key: &VerifyingKey) -> Result<bool, Box<dyn Error>> {
        let mut out = Vec::new();
        let mut writer = Writer::new(&mut out);
        self.get_raw_details().write_message(&mut writer)?;

        let sig = Signature::from_slice(&self.signature)?;

        Ok(key.verify(&out, &sig).is_ok())
    }

    /// Returns true if the signature is too young or too old compared to the provided time
    pub fn expired(&self, time: SystemTime) -> bool {
        self.details.not_before > time || self.details.not_after < time
    }

    /// Verify will ensure a certificate is good in all respects (expiry, group membership, signature, cert blocklist, etc)
    /// # Errors
    /// This function will return an error if there is an error parsing the cert or the CA pool.
    pub fn verify(&self, time: SystemTime, ca_pool: &NebulaCAPool) -> Result<CertificateValidity, Box<dyn Error>> {
        if ca_pool.is_blocklisted(self) {
            return Ok(CertificateValidity::Blocklisted);
        }

        let Some(signer) = ca_pool.get_ca_for_cert(self)? else { return Ok(CertificateValidity::NotSignedByThisCAPool) };

        if signer.expired(time) {
            return Ok(CertificateValidity::RootCertExpired)
        }

        if self.expired(time) {
            return Ok(CertificateValidity::CertExpired)
        }

        if !self.check_signature(&VerifyingKey::from_bytes(&signer.details.public_key)?)? {
            return Ok(CertificateValidity::BadSignature)
        }

        Ok(self.check_root_constraints(signer))
    }

    /// Make sure that this certificate does not break any of the constraints set by the signing certificate
    pub fn check_root_constraints(&self, signer: &Self) -> CertificateValidity {
        // Make sure this cert doesn't expire after the signer
        println!("{:?} {:?}", signer.details.not_before, self.details.not_before);
        if signer.details.not_before < self.details.not_before {
            return CertificateValidity::CertExpiresAfterSigner;
        }

        // Make sure this cert doesn't come into validity before the root
        if signer.details.not_before > self.details.not_before {
            return CertificateValidity::CertValidBeforeSigner;
        }

        // If the signer contains a limited set of groups, make sure this cert only has a subset of them
        if !signer.details.groups.is_empty() {
            println!("root groups: {:?}, child groups: {:?}", signer.details.groups, self.details.groups);
            for group in &self.details.groups {
                if !signer.details.groups.contains(group) {
                    return CertificateValidity::GroupNotPresentOnSigner;
                }
            }
        }

        // If the signer contains a limited set of IP ranges, make sure the cert only contains a subset
        if !signer.details.ips.is_empty() {
            for ip in &self.details.ips {
                if !net_match(*ip, &signer.details.ips) {
                    return CertificateValidity::IPNotPresentOnSigner;
                }
            }
        }

        // If the signer contains a limited set of subnets, make sure the cert only contains a subset
        if !signer.details.subnets.is_empty() {
            for subnet in &self.details.subnets {
                if !net_match(*subnet, &signer.details.subnets) {
                    return CertificateValidity::SubnetNotPresentOnSigner;
                }
            }
        }

        CertificateValidity::Ok
    }

    #[allow(clippy::unwrap_used)]
    /// Verify if the given private key corresponds to the public key contained in this certificate
    /// # Errors
    /// This function will return an error if either keys are invalid.
    /// # Panics
    /// This function, while containing calls to unwrap, has proper bounds checking and will not panic.
    pub fn verify_private_key(&self, key: &[u8]) -> Result<(), Box<dyn Error>> {
        if self.details.is_ca {
            // convert the keys
            if key.len() != 64 {
                return Err("key not 64-bytes long".into())
            }


            let secret = SigningKey::from_keypair_bytes(key.try_into().unwrap())?;
            let pub_key = secret.verifying_key().to_bytes();
            if pub_key != self.details.public_key {
                return Err(CertificateError::KeyMismatch.into());
            }

            return Ok(());
        }

        if key.len() != 32 {
            return Err("key not 32-bytes long".into())
        }

        let pubkey_raw = SigningKey::from_bytes(key.try_into()?).verifying_key();
        let pubkey = pubkey_raw.as_bytes();

        println!("{} {}", hex::encode(pubkey), hex::encode(self.details.public_key));
        if *pubkey != self.details.public_key {
            return Err(CertificateError::KeyMismatch.into());
        }

        Ok(())
    }


    /// Get a protobuf-ready raw struct, ready for serialization
    #[allow(clippy::expect_used)]
    #[allow(clippy::cast_possible_wrap)]
    /// # Panics
    /// This function will panic if time went backwards, or if the certificate contains extremely invalid data.
    pub fn get_raw_details(&self) -> RawNebulaCertificateDetails {

        let mut raw = RawNebulaCertificateDetails {
            Name: self.details.name.clone(),
            Ips: vec![],
            Subnets: vec![],
            Groups: self.details.groups.iter().map(std::convert::Into::into).collect(),
            NotBefore: self.details.not_before.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64,
            NotAfter: self.details.not_after.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64,
            PublicKey: self.details.public_key.into(),
            IsCA: self.details.is_ca,
            Issuer: hex::decode(&self.details.issuer).expect("Issuer was not a hex-encoded value"),
        };

        for ip_net in &self.details.ips {
            raw.Ips.push(ip_net.addr().into());
            raw.Ips.push(ip_net.netmask().into());
        }

        for subnet in &self.details.subnets {
            raw.Subnets.push(subnet.addr().into());
            raw.Subnets.push(subnet.netmask().into());
        }

        raw
    }

    /// Will serialize this cert into a protobuf byte array.
    /// # Errors
    /// This function will return an error if protobuf was unable to serialize the data.
    pub fn serialize(&self) -> Result<Vec<u8>, Box<dyn Error>> {
        let raw_cert = RawNebulaCertificate {
            Details: Some(self.get_raw_details()),
            Signature: self.signature.clone(),
        };

        let mut out = vec![];
        let mut writer = Writer::new(&mut out);
        raw_cert.write_message(&mut writer)?;

        Ok(out)
    }

    /// Will serialize this cert into a PEM byte array.
    /// # Errors
    /// This function will return an error if protobuf was unable to serialize the data.
    pub fn serialize_to_pem(&self) -> Result<Vec<u8>, Box<dyn Error>> {
        let pbuf_bytes = self.serialize()?;

        Ok(pem::encode(&Pem {
            tag: CERT_BANNER.to_string(),
            contents: pbuf_bytes,
        }).as_bytes().to_vec())
    }

    /// Get the fingerprint of this certificate
    /// # Errors
    /// This functiom will return an error if protobuf was unable to serialize the cert.
    pub fn sha256sum(&self) -> Result<String, Box<dyn Error>> {
        let pbuf_bytes = self.serialize()?;

        let mut hasher = Sha256::new();
        hasher.update(pbuf_bytes);

        Ok(hex::encode(hasher.finalize()))
    }
}

/// A list of possible errors that can happen validating a certificate
#[derive(Eq, PartialEq, Debug)]
pub enum CertificateValidity {
    /// There are no issues with this certificate
    Ok,
    /// This cert has been blocklisted in the given CA pool
    Blocklisted,
    /// The certificate that signed this cert is expired
    RootCertExpired,
    /// This cert is expired
    CertExpired,
    /// This cert's signature is invalid
    BadSignature,
    /// This cert was not signed by any CAs in the CA pool
    NotSignedByThisCAPool,
    /// This cert expires after the signer's cert expires
    CertExpiresAfterSigner,
    /// This cert enters validity before the signer's cert does
    CertValidBeforeSigner,
    /// A group present on this certificate is not present on the signer's certificate
    GroupNotPresentOnSigner,
    /// An IP present on this certificate is not present on the signer's certificate
    IPNotPresentOnSigner,
    /// A subnet on this certificate is not present on the signer's certificate
    SubnetNotPresentOnSigner
}

fn net_match(cert_ip: Ipv4Net, root_ips: &Vec<Ipv4Net>) -> bool {
    for net in root_ips {
        if net.contains(&cert_ip) {
            return true;
        }
    }
    false
}