// This file is dual licensed under the terms of the Apache License, Version
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
// for complete details.

use crate::asn1::{
    big_byte_slice_to_py_int, py_uint_to_big_endian_bytes, PyAsn1Error, PyAsn1Result,
};
use crate::x509;
use crate::x509::{crl, extensions, oid, sct};
use chrono::Datelike;
use pyo3::ToPyObject;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::Arc;

#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq, Clone)]
pub(crate) struct RawCertificate<'a> {
    pub(crate) tbs_cert: TbsCertificate<'a>,
    signature_alg: x509::AlgorithmIdentifier<'a>,
    signature: asn1::BitString<'a>,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq, Clone)]
pub(crate) struct TbsCertificate<'a> {
    #[explicit(0)]
    #[default(0)]
    version: u8,
    pub(crate) serial: asn1::BigInt<'a>,
    signature_alg: x509::AlgorithmIdentifier<'a>,

    pub(crate) issuer: x509::Name<'a>,
    validity: Validity,
    pub(crate) subject: x509::Name<'a>,

    pub(crate) spki: SubjectPublicKeyInfo<'a>,
    #[implicit(1)]
    issuer_unique_id: Option<asn1::BitString<'a>>,
    #[implicit(2)]
    subject_unique_id: Option<asn1::BitString<'a>>,
    #[explicit(3)]
    extensions: Option<x509::Extensions<'a>>,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq, Clone)]
pub(crate) struct Validity {
    not_before: x509::Time,
    not_after: x509::Time,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq, Clone)]
pub(crate) struct SubjectPublicKeyInfo<'a> {
    _algorithm: x509::AlgorithmIdentifier<'a>,
    pub(crate) subject_public_key: asn1::BitString<'a>,
}

#[ouroboros::self_referencing]
pub(crate) struct OwnedRawCertificate {
    data: Arc<[u8]>,

    #[borrows(data)]
    #[covariant]
    value: RawCertificate<'this>,
}

impl OwnedRawCertificate {
    // Re-expose ::new with `pub(crate)` visibility.
    pub(crate) fn new_public(
        data: Arc<[u8]>,
        value_ref_builder: impl for<'this> FnOnce(&'this Arc<[u8]>) -> RawCertificate<'this>,
    ) -> OwnedRawCertificate {
        OwnedRawCertificate::new(data, value_ref_builder)
    }

    pub(crate) fn borrow_value_public(&self) -> &RawCertificate<'_> {
        self.borrow_value()
    }
}

#[pyo3::prelude::pyclass]
pub(crate) struct Certificate {
    pub(crate) raw: OwnedRawCertificate,
    pub(crate) cached_extensions: Option<pyo3::PyObject>,
}

#[pyo3::prelude::pyproto]
impl pyo3::PyObjectProtocol for Certificate {
    fn __hash__(&self) -> u64 {
        let mut hasher = DefaultHasher::new();
        self.raw.borrow_value().hash(&mut hasher);
        hasher.finish()
    }

    fn __richcmp__(
        &self,
        other: pyo3::PyRef<Certificate>,
        op: pyo3::basic::CompareOp,
    ) -> pyo3::PyResult<bool> {
        match op {
            pyo3::basic::CompareOp::Eq => Ok(self.raw.borrow_value() == other.raw.borrow_value()),
            pyo3::basic::CompareOp::Ne => Ok(self.raw.borrow_value() != other.raw.borrow_value()),
            _ => Err(pyo3::exceptions::PyTypeError::new_err(
                "Certificates cannot be ordered",
            )),
        }
    }

    fn __repr__(&self) -> pyo3::PyResult<String> {
        let gil = pyo3::Python::acquire_gil();
        let py = gil.python();

        let subject = self.subject(py)?;
        let subject_repr = subject.repr()?.extract::<&str>()?;
        Ok(format!("<Certificate(subject={}, ...)>", subject_repr))
    }
}

#[pyo3::prelude::pymethods]
impl Certificate {
    fn __deepcopy__(slf: pyo3::PyRef<'_, Self>, _memo: pyo3::PyObject) -> pyo3::PyRef<'_, Self> {
        slf
    }

    fn public_key<'p>(&self, py: pyo3::Python<'p>) -> pyo3::PyResult<&'p pyo3::PyAny> {
        // This makes an unnecessary copy. It'd be nice to get rid of it.
        let serialized = pyo3::types::PyBytes::new(
            py,
            &asn1::write_single(&self.raw.borrow_value().tbs_cert.spki),
        );
        py.import("cryptography.hazmat.primitives.serialization")?
            .getattr("load_der_public_key")?
            .call1((serialized,))
    }

    fn fingerprint<'p>(
        &self,
        py: pyo3::Python<'p>,
        algorithm: pyo3::PyObject,
    ) -> pyo3::PyResult<&'p pyo3::PyAny> {
        let hasher = py
            .import("cryptography.hazmat.primitives.hashes")?
            .getattr("Hash")?
            .call1((algorithm,))?;
        // This makes an unnecessary copy. It'd be nice to get rid of it.
        let serialized =
            pyo3::types::PyBytes::new(py, &asn1::write_single(&self.raw.borrow_value()));
        hasher.call_method1("update", (serialized,))?;
        hasher.call_method0("finalize")
    }

    fn public_bytes<'p>(
        &self,
        py: pyo3::Python<'p>,
        encoding: &pyo3::PyAny,
    ) -> pyo3::PyResult<&'p pyo3::types::PyBytes> {
        let encoding_class = py
            .import("cryptography.hazmat.primitives.serialization")?
            .getattr("Encoding")?;

        let result = asn1::write_single(self.raw.borrow_value());
        if encoding == encoding_class.getattr("DER")? {
            Ok(pyo3::types::PyBytes::new(py, &result))
        } else if encoding == encoding_class.getattr("PEM")? {
            let pem = pem::encode_config(
                &pem::Pem {
                    tag: "CERTIFICATE".to_string(),
                    contents: result,
                },
                pem::EncodeConfig {
                    line_ending: pem::LineEnding::LF,
                },
            )
            .into_bytes();
            Ok(pyo3::types::PyBytes::new(py, &pem))
        } else {
            Err(pyo3::exceptions::PyTypeError::new_err(
                "encoding must be Encoding.DER or Encoding.PEM",
            ))
        }
    }

    #[getter]
    fn serial_number<'p>(&self, py: pyo3::Python<'p>) -> Result<&'p pyo3::PyAny, PyAsn1Error> {
        let bytes = self.raw.borrow_value().tbs_cert.serial.as_bytes();
        warn_if_negative_serial(py, bytes)?;
        Ok(big_byte_slice_to_py_int(py, bytes)?)
    }

    #[getter]
    fn version<'p>(&self, py: pyo3::Python<'p>) -> Result<&'p pyo3::PyAny, PyAsn1Error> {
        let version = &self.raw.borrow_value().tbs_cert.version;
        cert_version(py, *version)
    }

    #[getter]
    fn issuer<'p>(&self, py: pyo3::Python<'p>) -> pyo3::PyResult<&'p pyo3::PyAny> {
        Ok(
            x509::parse_name(py, &self.raw.borrow_value().tbs_cert.issuer)
                .map_err(|e| e.add_location(asn1::ParseLocation::Field("issuer")))?,
        )
    }

    #[getter]
    fn subject<'p>(&self, py: pyo3::Python<'p>) -> pyo3::PyResult<&'p pyo3::PyAny> {
        Ok(
            x509::parse_name(py, &self.raw.borrow_value().tbs_cert.subject)
                .map_err(|e| e.add_location(asn1::ParseLocation::Field("subject")))?,
        )
    }

    #[getter]
    fn tbs_certificate_bytes<'p>(
        &self,
        py: pyo3::Python<'p>,
    ) -> Result<&'p pyo3::types::PyBytes, PyAsn1Error> {
        let result = asn1::write_single(&self.raw.borrow_value().tbs_cert);
        Ok(pyo3::types::PyBytes::new(py, &result))
    }

    #[getter]
    fn signature<'p>(&self, py: pyo3::Python<'p>) -> Result<&'p pyo3::types::PyBytes, PyAsn1Error> {
        Ok(pyo3::types::PyBytes::new(
            py,
            self.raw.borrow_value().signature.as_bytes(),
        ))
    }

    #[getter]
    fn not_valid_before<'p>(&self, py: pyo3::Python<'p>) -> pyo3::PyResult<&'p pyo3::PyAny> {
        let chrono = &self
            .raw
            .borrow_value()
            .tbs_cert
            .validity
            .not_before
            .as_chrono();
        x509::chrono_to_py(py, chrono)
    }

    #[getter]
    fn not_valid_after<'p>(&self, py: pyo3::Python<'p>) -> pyo3::PyResult<&'p pyo3::PyAny> {
        let chrono = &self
            .raw
            .borrow_value()
            .tbs_cert
            .validity
            .not_after
            .as_chrono();
        x509::chrono_to_py(py, chrono)
    }

    #[getter]
    fn signature_hash_algorithm<'p>(
        &self,
        py: pyo3::Python<'p>,
    ) -> Result<&'p pyo3::PyAny, PyAsn1Error> {
        let sig_oids_to_hash = py
            .import("cryptography.hazmat._oid")?
            .getattr("_SIG_OIDS_TO_HASH")?;
        let hash_alg = sig_oids_to_hash.get_item(self.signature_algorithm_oid(py)?);
        match hash_alg {
            Ok(data) => Ok(data),
            Err(_) => Err(PyAsn1Error::from(pyo3::PyErr::from_instance(
                py.import("cryptography.exceptions")?.call_method1(
                    "UnsupportedAlgorithm",
                    (format!(
                        "Signature algorithm OID: {} not recognized",
                        self.raw.borrow_value().signature_alg.oid
                    ),),
                )?,
            ))),
        }
    }

    #[getter]
    fn signature_algorithm_oid<'p>(&self, py: pyo3::Python<'p>) -> pyo3::PyResult<&'p pyo3::PyAny> {
        py.import("cryptography.x509")?.call_method1(
            "ObjectIdentifier",
            (self.raw.borrow_value().signature_alg.oid.to_string(),),
        )
    }

    #[getter]
    fn extensions(&mut self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::PyObject> {
        let x509_module = py.import("cryptography.x509")?;
        x509::parse_and_cache_extensions(
            py,
            &mut self.cached_extensions,
            &self.raw.borrow_value().tbs_cert.extensions,
            |oid, ext_data| {
                if oid == &*oid::PRECERT_POISON_OID {
                    asn1::parse_single::<()>(ext_data)?;
                    Ok(Some(x509_module.getattr("PrecertPoison")?.call0()?))
                } else if oid == &*oid::PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS_OID {
                    let contents = asn1::parse_single::<&[u8]>(ext_data)?;
                    let scts = sct::parse_scts(py, contents, sct::LogEntryType::PreCertificate)?;
                    Ok(Some(
                        x509_module
                            .getattr("PrecertificateSignedCertificateTimestamps")?
                            .call1((scts,))?,
                    ))
                } else {
                    parse_cert_ext(py, oid.clone(), ext_data)
                }
            },
        )
    }
    // This getter exists for compatibility with pyOpenSSL and will be removed.
    // DO NOT RELY ON IT. WE WILL BREAK YOU WHEN WE FEEL LIKE IT.
    #[getter]
    fn _x509<'p>(
        slf: pyo3::PyRef<'_, Self>,
        py: pyo3::Python<'p>,
    ) -> Result<&'p pyo3::PyAny, PyAsn1Error> {
        let cryptography_warning = py.import("cryptography.utils")?.getattr("DeprecatedIn35")?;
        let warnings = py.import("warnings")?;
        warnings.call_method1(
            "warn",
            (
                "This version of cryptography contains a temporary pyOpenSSL fallback path. Upgrade pyOpenSSL now.",
                cryptography_warning,
            ),
        )?;
        let backend = py
            .import("cryptography.hazmat.backends.openssl.backend")?
            .getattr("backend")?;
        Ok(backend.call_method1("_cert2ossl", (slf,))?)
    }
}

fn cert_version(py: pyo3::Python<'_>, version: u8) -> Result<&pyo3::PyAny, PyAsn1Error> {
    let x509_module = py.import("cryptography.x509")?;
    match version {
        0 => Ok(x509_module.getattr("Version")?.get_item("v1")?),
        2 => Ok(x509_module.getattr("Version")?.get_item("v3")?),
        _ => Err(PyAsn1Error::from(pyo3::PyErr::from_instance(
            x509_module
                .getattr("InvalidVersion")?
                .call1((format!("{} is not a valid X509 version", version), version))?,
        ))),
    }
}

#[pyo3::prelude::pyfunction]
fn load_pem_x509_certificate(py: pyo3::Python<'_>, data: &[u8]) -> PyAsn1Result<Certificate> {
    // We support both PEM header strings that OpenSSL does
    // https://github.com/openssl/openssl/blob/5e2d22d53ed322a7124e26a4fbd116a8210eb77a/include/openssl/pem.h#L32-L33
    let parsed = x509::find_in_pem(
        data,
        |p| p.tag == "CERTIFICATE" || p.tag == "X509 CERTIFICATE",
        "Valid PEM but no BEGIN CERTIFICATE/END CERTIFICATE delimiters. Are you sure this is a certificate?",
    )?;
    load_der_x509_certificate(py, &parsed.contents)
}

#[pyo3::prelude::pyfunction]
fn load_der_x509_certificate(py: pyo3::Python<'_>, data: &[u8]) -> PyAsn1Result<Certificate> {
    let raw = OwnedRawCertificate::try_new(Arc::from(data), |data| asn1::parse_single(data))?;
    // Parse cert version immediately so we can raise error on parse if it is invalid.
    cert_version(py, raw.borrow_value().tbs_cert.version)?;
    // determine if the serial is negative and raise a warning if it is. We want to drop support
    // for this sort of invalid encoding eventually.
    warn_if_negative_serial(py, raw.borrow_value().tbs_cert.serial.as_bytes())?;

    Ok(Certificate {
        raw,
        cached_extensions: None,
    })
}

fn warn_if_negative_serial(py: pyo3::Python<'_>, bytes: &'_ [u8]) -> pyo3::PyResult<()> {
    if bytes[0] & 0x80 != 0 {
        let cryptography_warning = py.import("cryptography.utils")?.getattr("DeprecatedIn36")?;
        let warnings = py.import("warnings")?;
        warnings.call_method1(
            "warn",
            (
                "Parsed a negative serial number, which is disallowed by RFC 5280.",
                cryptography_warning,
            ),
        )?;
    }
    Ok(())
}

// Needed due to clippy type complexity warning.
type SequenceOfPolicyQualifiers<'a> = x509::Asn1ReadableOrWritable<
    'a,
    asn1::SequenceOf<'a, PolicyQualifierInfo<'a>>,
    asn1::SequenceOfWriter<'a, PolicyQualifierInfo<'a>, Vec<PolicyQualifierInfo<'a>>>,
>;

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct PolicyInformation<'a> {
    pub policy_identifier: asn1::ObjectIdentifier<'a>,
    pub policy_qualifiers: Option<SequenceOfPolicyQualifiers<'a>>,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct PolicyQualifierInfo<'a> {
    pub policy_qualifier_id: asn1::ObjectIdentifier<'a>,
    pub qualifier: Qualifier<'a>,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) enum Qualifier<'a> {
    CpsUri(asn1::IA5String<'a>),
    UserNotice(UserNotice<'a>),
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct UserNotice<'a> {
    pub notice_ref: Option<NoticeReference<'a>>,
    pub explicit_text: Option<DisplayText<'a>>,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct NoticeReference<'a> {
    pub organization: DisplayText<'a>,
    pub notice_numbers: x509::Asn1ReadableOrWritable<
        'a,
        asn1::SequenceOf<'a, asn1::BigUint<'a>>,
        asn1::SequenceOfWriter<'a, asn1::BigUint<'a>, Vec<asn1::BigUint<'a>>>,
    >,
}

// DisplayText also allows BMPString, which we currently do not support.
#[allow(clippy::enum_variant_names)]
#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) enum DisplayText<'a> {
    IA5String(asn1::IA5String<'a>),
    Utf8String(asn1::Utf8String<'a>),
    VisibleString(asn1::VisibleString<'a>),
    BmpString(asn1::BMPString<'a>),
}

fn parse_display_text(
    py: pyo3::Python<'_>,
    text: DisplayText<'_>,
) -> pyo3::PyResult<pyo3::PyObject> {
    match text {
        DisplayText::IA5String(o) => Ok(pyo3::types::PyString::new(py, o.as_str()).to_object(py)),
        DisplayText::Utf8String(o) => Ok(pyo3::types::PyString::new(py, o.as_str()).to_object(py)),
        DisplayText::VisibleString(o) => {
            Ok(pyo3::types::PyString::new(py, o.as_str()).to_object(py))
        }
        DisplayText::BmpString(o) => {
            let py_bytes = pyo3::types::PyBytes::new(py, o.as_utf16_be_bytes());
            // TODO: do the string conversion in rust perhaps
            Ok(py_bytes
                .call_method1("decode", ("utf_16_be",))?
                .to_object(py))
        }
    }
}

fn parse_user_notice(
    py: pyo3::Python<'_>,
    un: UserNotice<'_>,
) -> Result<pyo3::PyObject, PyAsn1Error> {
    let x509_module = py.import("cryptography.x509")?;
    let et = match un.explicit_text {
        Some(data) => parse_display_text(py, data)?,
        None => py.None(),
    };
    let nr = match un.notice_ref {
        Some(data) => {
            let org = parse_display_text(py, data.organization)?;
            let numbers = pyo3::types::PyList::empty(py);
            for num in data.notice_numbers.unwrap_read().clone() {
                numbers.append(big_byte_slice_to_py_int(py, num.as_bytes())?.to_object(py))?;
            }
            x509_module
                .call_method1("NoticeReference", (org, numbers))?
                .to_object(py)
        }
        None => py.None(),
    };
    Ok(x509_module
        .call_method1("UserNotice", (nr, et))?
        .to_object(py))
}

fn parse_policy_qualifiers<'a>(
    py: pyo3::Python<'_>,
    policy_qualifiers: &asn1::SequenceOf<'a, PolicyQualifierInfo<'a>>,
) -> Result<pyo3::PyObject, PyAsn1Error> {
    let py_pq = pyo3::types::PyList::empty(py);
    for pqi in policy_qualifiers.clone() {
        let qualifier = match pqi.qualifier {
            Qualifier::CpsUri(data) => {
                if pqi.policy_qualifier_id == *oid::CP_CPS_URI_OID {
                    pyo3::types::PyString::new(py, data.as_str()).to_object(py)
                } else {
                    return Err(PyAsn1Error::from(pyo3::exceptions::PyValueError::new_err(
                        "CpsUri ASN.1 structure found but OID did not match",
                    )));
                }
            }
            Qualifier::UserNotice(un) => {
                if pqi.policy_qualifier_id != *oid::CP_USER_NOTICE_OID {
                    return Err(PyAsn1Error::from(pyo3::exceptions::PyValueError::new_err(
                        "UserNotice ASN.1 structure found but OID did not match",
                    )));
                }
                parse_user_notice(py, un)?
            }
        };
        py_pq.append(qualifier)?;
    }
    Ok(py_pq.to_object(py))
}

fn parse_cp(py: pyo3::Python<'_>, ext_data: &[u8]) -> Result<pyo3::PyObject, PyAsn1Error> {
    let cp = asn1::parse_single::<asn1::SequenceOf<'_, PolicyInformation<'_>>>(ext_data)?;
    let x509_module = py.import("cryptography.x509")?;
    let certificate_policies = pyo3::types::PyList::empty(py);
    for policyinfo in cp {
        let pi_oid = x509_module
            .call_method1(
                "ObjectIdentifier",
                (policyinfo.policy_identifier.to_string(),),
            )?
            .to_object(py);
        let py_pqis = match policyinfo.policy_qualifiers {
            Some(policy_qualifiers) => {
                parse_policy_qualifiers(py, policy_qualifiers.unwrap_read())?
            }
            None => py.None(),
        };
        let pi = x509_module
            .call_method1("PolicyInformation", (pi_oid, py_pqis))?
            .to_object(py);
        certificate_policies.append(pi)?;
    }
    Ok(certificate_policies.to_object(py))
}

// Needed due to clippy type complexity warning.
pub(crate) type SequenceOfSubtrees<'a> = x509::Asn1ReadableOrWritable<
    'a,
    asn1::SequenceOf<'a, GeneralSubtree<'a>>,
    asn1::SequenceOfWriter<'a, GeneralSubtree<'a>, Vec<GeneralSubtree<'a>>>,
>;

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct NameConstraints<'a> {
    #[implicit(0)]
    pub permitted_subtrees: Option<SequenceOfSubtrees<'a>>,

    #[implicit(1)]
    pub excluded_subtrees: Option<SequenceOfSubtrees<'a>>,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct GeneralSubtree<'a> {
    pub base: x509::GeneralName<'a>,

    #[implicit(0)]
    #[default(0u64)]
    pub minimum: u64,

    #[implicit(1)]
    pub maximum: Option<u64>,
}

fn parse_general_subtrees(
    py: pyo3::Python<'_>,
    subtrees: SequenceOfSubtrees<'_>,
) -> Result<pyo3::PyObject, PyAsn1Error> {
    let gns = pyo3::types::PyList::empty(py);
    for gs in subtrees.unwrap_read().clone() {
        gns.append(x509::parse_general_name(py, gs.base)?)?;
    }
    Ok(gns.to_object(py))
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct DistributionPoint<'a> {
    #[explicit(0)]
    pub distribution_point: Option<DistributionPointName<'a>>,

    #[implicit(1)]
    pub reasons: crl::ReasonFlags<'a>,

    #[implicit(2)]
    pub crl_issuer: Option<x509::common::SequenceOfGeneralName<'a>>,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) enum DistributionPointName<'a> {
    #[implicit(0)]
    FullName(x509::common::SequenceOfGeneralName<'a>),

    #[implicit(1)]
    NameRelativeToCRLIssuer(
        x509::Asn1ReadableOrWritable<
            'a,
            asn1::SetOf<'a, x509::AttributeTypeValue<'a>>,
            asn1::SetOfWriter<'a, x509::AttributeTypeValue<'a>, Vec<x509::AttributeTypeValue<'a>>>,
        >,
    ),
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct AuthorityKeyIdentifier<'a> {
    #[implicit(0)]
    pub key_identifier: Option<&'a [u8]>,
    #[implicit(1)]
    pub authority_cert_issuer: Option<x509::common::SequenceOfGeneralName<'a>>,
    #[implicit(2)]
    pub authority_cert_serial_number: Option<asn1::BigUint<'a>>,
}

pub(crate) fn parse_distribution_point_name(
    py: pyo3::Python<'_>,
    dp: DistributionPointName<'_>,
) -> Result<(pyo3::PyObject, pyo3::PyObject), PyAsn1Error> {
    Ok(match dp {
        DistributionPointName::FullName(data) => (
            x509::parse_general_names(py, data.unwrap_read())?,
            py.None(),
        ),
        DistributionPointName::NameRelativeToCRLIssuer(data) => {
            (py.None(), x509::parse_rdn(py, data.unwrap_read())?)
        }
    })
}

fn parse_distribution_point(
    py: pyo3::Python<'_>,
    dp: DistributionPoint<'_>,
) -> Result<pyo3::PyObject, PyAsn1Error> {
    let (full_name, relative_name) = match dp.distribution_point {
        Some(data) => parse_distribution_point_name(py, data)?,
        None => (py.None(), py.None()),
    };
    let reasons =
        parse_distribution_point_reasons(py, dp.reasons.as_ref().map(|v| v.unwrap_read()))?;
    let crl_issuer = match dp.crl_issuer {
        Some(aci) => x509::parse_general_names(py, aci.unwrap_read())?,
        None => py.None(),
    };
    let x509_module = py.import("cryptography.x509")?;
    Ok(x509_module
        .getattr("DistributionPoint")?
        .call1((full_name, relative_name, reasons, crl_issuer))?
        .to_object(py))
}

pub(crate) fn parse_distribution_points(
    py: pyo3::Python<'_>,
    data: &[u8],
) -> Result<pyo3::PyObject, PyAsn1Error> {
    let dps = asn1::parse_single::<asn1::SequenceOf<'_, DistributionPoint<'_>>>(data)?;
    let py_dps = pyo3::types::PyList::empty(py);
    for dp in dps {
        let py_dp = parse_distribution_point(py, dp)?;
        py_dps.append(py_dp)?;
    }
    Ok(py_dps.to_object(py))
}

pub(crate) fn parse_distribution_point_reasons(
    py: pyo3::Python<'_>,
    reasons: Option<&asn1::BitString<'_>>,
) -> Result<pyo3::PyObject, PyAsn1Error> {
    let reason_bit_mapping = py
        .import("cryptography.x509.extensions")?
        .getattr("_REASON_BIT_MAPPING")?;
    Ok(match reasons {
        Some(bs) => {
            let mut vec = Vec::new();
            for i in 1..=8 {
                if bs.has_bit_set(i) {
                    vec.push(reason_bit_mapping.get_item(i)?);
                }
            }
            pyo3::types::PyFrozenSet::new(py, &vec)?.to_object(py)
        }
        None => py.None(),
    })
}

pub(crate) fn encode_distribution_point_reasons(
    py: pyo3::Python<'_>,
    py_reasons: &pyo3::PyAny,
) -> pyo3::PyResult<asn1::OwnedBitString> {
    let reason_flag_mapping = py
        .import("cryptography.hazmat.backends.openssl.encode_asn1")?
        .getattr("_CRLREASONFLAGS")?;

    let mut bits = vec![0, 0];
    for py_reason in py_reasons.iter()? {
        let bit = reason_flag_mapping
            .get_item(py_reason?)?
            .extract::<usize>()?;
        set_bit(&mut bits, bit, true)
    }
    if bits[1] == 0 {
        bits.truncate(1);
    }
    let unused_bits = bits.last().unwrap().trailing_zeros() as u8;
    Ok(asn1::OwnedBitString::new(bits, unused_bits).unwrap())
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct BasicConstraints {
    #[default(false)]
    pub ca: bool,
    pub path_length: Option<u64>,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub(crate) struct PolicyConstraints {
    #[implicit(0)]
    pub require_explicit_policy: Option<u64>,
    #[implicit(1)]
    pub inhibit_policy_mapping: Option<u64>,
}

pub(crate) fn parse_authority_key_identifier<'p>(
    py: pyo3::Python<'p>,
    ext_data: &[u8],
) -> Result<&'p pyo3::PyAny, PyAsn1Error> {
    let x509_module = py.import("cryptography.x509")?;
    let aki = asn1::parse_single::<AuthorityKeyIdentifier<'_>>(ext_data)?;
    let serial = match aki.authority_cert_serial_number {
        Some(biguint) => big_byte_slice_to_py_int(py, biguint.as_bytes())?.to_object(py),
        None => py.None(),
    };
    let issuer = match aki.authority_cert_issuer {
        Some(aci) => x509::parse_general_names(py, aci.unwrap_read())?,
        None => py.None(),
    };
    Ok(x509_module.getattr("AuthorityKeyIdentifier")?.call1((
        aki.key_identifier,
        issuer,
        serial,
    ))?)
}

pub(crate) fn parse_access_descriptions(
    py: pyo3::Python<'_>,
    ext_data: &[u8],
) -> Result<pyo3::PyObject, PyAsn1Error> {
    let x509_module = py.import("cryptography.x509")?;
    let ads = pyo3::types::PyList::empty(py);
    let parsed = asn1::parse_single::<x509::common::SequenceOfAccessDescriptions<'_>>(ext_data)?;
    for access in parsed.unwrap_read().clone() {
        let py_oid = x509_module
            .call_method1("ObjectIdentifier", (access.access_method.to_string(),))?
            .to_object(py);
        let gn = x509::parse_general_name(py, access.access_location)?;
        let ad = x509_module
            .getattr("AccessDescription")?
            .call1((py_oid, gn))?
            .to_object(py);
        ads.append(ad)?;
    }
    Ok(ads.to_object(py))
}

pub fn parse_cert_ext<'p>(
    py: pyo3::Python<'p>,
    oid: asn1::ObjectIdentifier<'_>,
    ext_data: &[u8],
) -> PyAsn1Result<Option<&'p pyo3::PyAny>> {
    let x509_module = py.import("cryptography.x509")?;
    if oid == *oid::SUBJECT_ALTERNATIVE_NAME_OID {
        let gn_seq = asn1::parse_single::<asn1::SequenceOf<'_, x509::GeneralName<'_>>>(ext_data)?;
        let sans = x509::parse_general_names(py, &gn_seq)?;
        Ok(Some(
            x509_module
                .getattr("SubjectAlternativeName")?
                .call1((sans,))?,
        ))
    } else if oid == *oid::ISSUER_ALTERNATIVE_NAME_OID {
        let gn_seq = asn1::parse_single::<asn1::SequenceOf<'_, x509::GeneralName<'_>>>(ext_data)?;
        let ians = x509::parse_general_names(py, &gn_seq)?;
        Ok(Some(
            x509_module
                .getattr("IssuerAlternativeName")?
                .call1((ians,))?,
        ))
    } else if oid == *oid::TLS_FEATURE_OID {
        let tls_feature_type_to_enum = py
            .import("cryptography.x509.extensions")?
            .getattr("_TLS_FEATURE_TYPE_TO_ENUM")?;

        let features = pyo3::types::PyList::empty(py);
        for feature in asn1::parse_single::<asn1::SequenceOf<'_, u64>>(ext_data)? {
            let py_feature = tls_feature_type_to_enum.get_item(feature.to_object(py))?;
            features.append(py_feature)?;
        }
        Ok(Some(x509_module.getattr("TLSFeature")?.call1((features,))?))
    } else if oid == *oid::SUBJECT_KEY_IDENTIFIER_OID {
        let identifier = asn1::parse_single::<&[u8]>(ext_data)?;
        Ok(Some(
            x509_module
                .getattr("SubjectKeyIdentifier")?
                .call1((identifier,))?,
        ))
    } else if oid == *oid::EXTENDED_KEY_USAGE_OID {
        let ekus = pyo3::types::PyList::empty(py);
        for oid in asn1::parse_single::<asn1::SequenceOf<'_, asn1::ObjectIdentifier<'_>>>(ext_data)?
        {
            let oid_obj = x509_module.call_method1("ObjectIdentifier", (oid.to_string(),))?;
            ekus.append(oid_obj)?;
        }
        Ok(Some(
            x509_module.getattr("ExtendedKeyUsage")?.call1((ekus,))?,
        ))
    } else if oid == *oid::KEY_USAGE_OID {
        let kus = asn1::parse_single::<asn1::BitString<'_>>(ext_data)?;
        let digital_signature = kus.has_bit_set(0);
        let content_comitment = kus.has_bit_set(1);
        let key_encipherment = kus.has_bit_set(2);
        let data_encipherment = kus.has_bit_set(3);
        let key_agreement = kus.has_bit_set(4);
        let key_cert_sign = kus.has_bit_set(5);
        let crl_sign = kus.has_bit_set(6);
        let encipher_only = kus.has_bit_set(7);
        let decipher_only = kus.has_bit_set(8);
        Ok(Some(x509_module.getattr("KeyUsage")?.call1((
            digital_signature,
            content_comitment,
            key_encipherment,
            data_encipherment,
            key_agreement,
            key_cert_sign,
            crl_sign,
            encipher_only,
            decipher_only,
        ))?))
    } else if oid == *oid::AUTHORITY_INFORMATION_ACCESS_OID {
        let ads = parse_access_descriptions(py, ext_data)?;
        Ok(Some(
            x509_module
                .getattr("AuthorityInformationAccess")?
                .call1((ads,))?,
        ))
    } else if oid == *oid::SUBJECT_INFORMATION_ACCESS_OID {
        let ads = parse_access_descriptions(py, ext_data)?;
        Ok(Some(
            x509_module
                .getattr("SubjectInformationAccess")?
                .call1((ads,))?,
        ))
    } else if oid == *oid::CERTIFICATE_POLICIES_OID {
        let cp = parse_cp(py, ext_data)?;
        Ok(Some(
            x509_module.call_method1("CertificatePolicies", (cp,))?,
        ))
    } else if oid == *oid::POLICY_CONSTRAINTS_OID {
        let pc = asn1::parse_single::<PolicyConstraints>(ext_data)?;
        Ok(Some(x509_module.getattr("PolicyConstraints")?.call1((
            pc.require_explicit_policy,
            pc.inhibit_policy_mapping,
        ))?))
    } else if oid == *oid::OCSP_NO_CHECK_OID {
        asn1::parse_single::<()>(ext_data)?;
        Ok(Some(x509_module.getattr("OCSPNoCheck")?.call0()?))
    } else if oid == *oid::INHIBIT_ANY_POLICY_OID {
        let bignum = asn1::parse_single::<asn1::BigUint<'_>>(ext_data)?;
        let pynum = big_byte_slice_to_py_int(py, bignum.as_bytes())?;
        Ok(Some(
            x509_module.getattr("InhibitAnyPolicy")?.call1((pynum,))?,
        ))
    } else if oid == *oid::BASIC_CONSTRAINTS_OID {
        let bc = asn1::parse_single::<BasicConstraints>(ext_data)?;
        Ok(Some(
            x509_module
                .getattr("BasicConstraints")?
                .call1((bc.ca, bc.path_length))?,
        ))
    } else if oid == *oid::AUTHORITY_KEY_IDENTIFIER_OID {
        Ok(Some(parse_authority_key_identifier(py, ext_data)?))
    } else if oid == *oid::CRL_DISTRIBUTION_POINTS_OID {
        let dp = parse_distribution_points(py, ext_data)?;
        Ok(Some(
            x509_module.getattr("CRLDistributionPoints")?.call1((dp,))?,
        ))
    } else if oid == *oid::FRESHEST_CRL_OID {
        let dp = parse_distribution_points(py, ext_data)?;
        Ok(Some(x509_module.getattr("FreshestCRL")?.call1((dp,))?))
    } else if oid == *oid::NAME_CONSTRAINTS_OID {
        let nc = asn1::parse_single::<NameConstraints<'_>>(ext_data)?;
        let permitted_subtrees = match nc.permitted_subtrees {
            Some(data) => parse_general_subtrees(py, data)?,
            None => py.None(),
        };
        let excluded_subtrees = match nc.excluded_subtrees {
            Some(data) => parse_general_subtrees(py, data)?,
            None => py.None(),
        };
        Ok(Some(
            x509_module
                .getattr("NameConstraints")?
                .call1((permitted_subtrees, excluded_subtrees))?,
        ))
    } else {
        Ok(None)
    }
}

pub(crate) fn time_from_py(val: &pyo3::PyAny) -> pyo3::PyResult<x509::Time> {
    let dt = x509::py_to_chrono(val)?;
    if dt.year() >= 2050 {
        Ok(x509::Time::GeneralizedTime(asn1::GeneralizedTime::new(dt)))
    } else {
        Ok(x509::Time::UtcTime(asn1::UtcTime::new(dt).unwrap()))
    }
}

#[pyo3::prelude::pyfunction]
fn create_x509_certificate(
    py: pyo3::Python<'_>,
    builder: &pyo3::PyAny,
    private_key: &pyo3::PyAny,
    hash_algorithm: &pyo3::PyAny,
) -> PyAsn1Result<Certificate> {
    let sigalg = x509::sign::compute_signature_algorithm(py, private_key, hash_algorithm)?;
    let serialization_mod = py.import("cryptography.hazmat.primitives.serialization")?;
    let der_encoding = serialization_mod.getattr("Encoding")?.getattr("DER")?;
    let spki_format = serialization_mod
        .getattr("PublicFormat")?
        .getattr("SubjectPublicKeyInfo")?;

    let spki_bytes = builder
        .getattr("_public_key")?
        .call_method1("public_bytes", (der_encoding, spki_format))?
        .extract::<&[u8]>()?;

    let py_serial = builder.getattr("_serial_number")?.extract()?;

    let tbs_cert = TbsCertificate {
        version: builder.getattr("_version")?.getattr("value")?.extract()?,
        serial: asn1::BigInt::new(py_uint_to_big_endian_bytes(py, py_serial)?).unwrap(),
        signature_alg: sigalg.clone(),
        issuer: x509::common::encode_name(py, builder.getattr("_issuer_name")?)?,
        validity: Validity {
            not_before: time_from_py(builder.getattr("_not_valid_before")?)?,
            not_after: time_from_py(builder.getattr("_not_valid_after")?)?,
        },
        subject: x509::common::encode_name(py, builder.getattr("_subject_name")?)?,
        spki: asn1::parse_single(spki_bytes)?,
        issuer_unique_id: None,
        subject_unique_id: None,
        extensions: x509::common::encode_extensions(
            py,
            builder.getattr("_extensions")?,
            extensions::encode_extension,
        )?,
    };

    let tbs_bytes = asn1::write_single(&tbs_cert);
    let signature = x509::sign::sign_data(py, private_key, hash_algorithm, &tbs_bytes)?;
    let data = asn1::write_single(&RawCertificate {
        tbs_cert,
        signature_alg: sigalg,
        signature: asn1::BitString::new(signature, 0).unwrap(),
    });
    // TODO: extra copy as we round-trip through a slice
    load_der_x509_certificate(py, &data)
}

pub(crate) fn set_bit(vals: &mut [u8], n: usize, set: bool) {
    let idx = n / 8;
    let v = 1 << (7 - (n & 0x07));
    if set {
        vals[idx] |= v;
    }
}

pub(crate) fn add_to_module(module: &pyo3::prelude::PyModule) -> pyo3::PyResult<()> {
    module.add_wrapped(pyo3::wrap_pyfunction!(load_der_x509_certificate))?;
    module.add_wrapped(pyo3::wrap_pyfunction!(load_pem_x509_certificate))?;
    module.add_wrapped(pyo3::wrap_pyfunction!(create_x509_certificate))?;

    module.add_class::<Certificate>()?;

    Ok(())
}
