use bindgen::callbacks::{IntKind, ParseCallbacks};
use std::path::{Path, PathBuf};

/// `OPENSSL_VERSION_NUMBER` is a hex integer macro of the form 0xMNN00PPSL.
/// We parse it in `int_macro` to enforce the minimum version floor.
#[derive(Debug)]
struct OsslCallbacks;

impl ParseCallbacks for OsslCallbacks {
    fn int_macro(&self, name: &str, value: i64) -> Option<IntKind> {
        if name == "OPENSSL_VERSION_NUMBER" {
            // 3.0.7 = 0x3000_0070
            assert!(
                value >= 0x3000_0070,
                "native-ossl requires OpenSSL >= 3.0.7; \
                 found OPENSSL_VERSION_NUMBER = {value:#010x}"
            );
            // ossl307 is always true at our 3.0.7 minimum floor.
            println!("cargo::rustc-cfg=ossl307");
            println!("cargo::metadata=OSSL307=1");
            // 3.1.0 = 0x3010_0000 — EVP_RAND_CTX_up_ref
            if value >= 0x3010_0000 {
                println!("cargo::rustc-cfg=ossl310");
                println!("cargo::metadata=OSSL310=1");
            }
            // 3.2.0 = 0x3020_0000 — EVP_PKEY_sign_message_*, encapsulate/decapsulate,
            //                        RAND_get0_public/private, OSSL_ERR_STATE_*
            if value >= 0x3020_0000 {
                println!("cargo::rustc-cfg=ossl320");
                println!("cargo::metadata=OSSL320=1");
            }
            // 3.5.0 = 0x3050_0000 — EVP_KDF_* SSH/KBKDF builders
            if value >= 0x3050_0000 {
                println!("cargo::rustc-cfg=ossl350");
                println!("cargo::metadata=OSSL350=1");
            }
            // 4.0.0 = 0x4000_0000 — EVP_MD_CTX_serialize/deserialize
            if value >= 0x4000_0000 {
                println!("cargo::rustc-cfg=ossl_v400");
                println!("cargo::metadata=OSSL_V400=1");
            }
        }
        None
    }

    // Detect algorithm-presence string macros from <openssl/core_names.h>.
    // These are defined as string constants when the corresponding algorithm
    // family is compiled into the installed OpenSSL.
    fn str_macro(&self, name: &str, _value: &[u8]) {
        match name {
            "OSSL_PKEY_PARAM_SLH_DSA_SEED" => {
                println!("cargo::rustc-cfg=ossl_slhdsa");
                println!("cargo::metadata=OSSL_SLHDSA=1");
            }
            "OSSL_PKEY_PARAM_ML_DSA_SEED" => {
                println!("cargo::rustc-cfg=ossl_mldsa");
                println!("cargo::metadata=OSSL_MLDSA=1");
            }
            "OSSL_PKEY_PARAM_ML_KEM_SEED" => {
                println!("cargo::rustc-cfg=ossl_mlkem");
                println!("cargo::metadata=OSSL_MLKEM=1");
            }
            _ => {}
        }
    }

    // Detect function-like macros.
    fn func_macro(&self, name: &str, _value: &[&[u8]]) {
        if name == "OSSL_PARAM_clear_free" {
            println!("cargo::rustc-cfg=param_clear_free");
            println!("cargo::metadata=PARAM_CLEAR_FREE=1");
        }
    }
}

// ── Allowlist helpers — one per API group ────────────────────────────────────

fn allow_error_param_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    b = b.allowlist_item("OPENSSL_VERSION_NUMBER");

    for sym in [
        "ERR_get_error_all",
        "ERR_peek_error",
        "ERR_clear_error",
        "ERR_error_string",
        "ERR_reason_error_string",
        "ERR_lib_error_string",
        "ERR_set_mark",
        "ERR_pop_to_mark",
        "OSSL_ERR_STATE_new",
        "OSSL_ERR_STATE_save",
        "OSSL_ERR_STATE_restore",
        "OSSL_ERR_STATE_free",
    ] {
        b = b.allowlist_function(sym);
    }
    for sym in ["ERR_GET_LIB", "ERR_GET_REASON", "ERR_GET_RFLAGS"] {
        b = b.allowlist_item(sym);
    }
    b = b.allowlist_type("ERR_STATE");

    for sym in [
        "OSSL_PARAM_BLD_new",
        "OSSL_PARAM_BLD_free",
        "OSSL_PARAM_BLD_push_int",
        "OSSL_PARAM_BLD_push_uint",
        "OSSL_PARAM_BLD_push_size_t",
        "OSSL_PARAM_BLD_push_utf8_string",
        "OSSL_PARAM_BLD_push_octet_string",
        "OSSL_PARAM_BLD_push_octet_ptr",
        "OSSL_PARAM_BLD_push_uint64",
        "OSSL_PARAM_BLD_to_param",
        "OSSL_PARAM_free",
        "OSSL_PARAM_get_int",
        "OSSL_PARAM_get_uint",
        "OSSL_PARAM_get_size_t",
        "OSSL_PARAM_get_octet_string_ptr",
        "OSSL_PARAM_get_utf8_string_ptr",
        "OSSL_PARAM_set_int",
        "OSSL_PARAM_set_uint",
        "OSSL_PARAM_set_size_t",
        "OSSL_PARAM_get_int64",
        "OSSL_PARAM_get_uint64",
        "OSSL_PARAM_get_long",
        "OSSL_PARAM_get_BN",
        "OSSL_PARAM_BLD_push_BN",
        "OSSL_PARAM_modified",
        "OSSL_PARAM_locate",
        "OPENSSL_cleanse",
        "CRYPTO_free",
        "CRYPTO_memcmp",
        // encrypted PEM write
        "PEM_write_bio_PKCS8PrivateKey",
        // unencrypted PKCS#8 DER export
        "i2d_PKCS8PrivateKeyInfo_bio",
        // PKCS#12 RFC 7292 Appendix B KDF
        "PKCS12_key_gen_utf8",
    ] {
        b = b.allowlist_item(sym);
    }
    b.allowlist_type("OSSL_PARAM")
        .allowlist_type("OSSL_PARAM_BLD")
}

fn allow_bio_libctx_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "BIO_new",
        "BIO_free",
        "BIO_free_all",
        "BIO_up_ref",
        "BIO_s_mem",
        "BIO_new_mem_buf",
        // BIO_get_mem_data is a C macro; expose BIO_ctrl instead.
        "BIO_ctrl",
        "BIO_read",
        "BIO_read_ex",
        "BIO_write",
        "BIO_write_ex",
        "BIO_pending",
        "BIO_wpending",
        "BIO_push",
        "BIO_pop",
        "BIO_next",
        "BIO_find_type",
        // BIO_CTRL_INFO is enum value 3; include the whole BIO_CTRL enum.
        "BIO_CTRL_INFO",
        "BIO_new_bio_pair",
    ] {
        b = b.allowlist_item(sym);
    }
    b = b
        .allowlist_type("BIO")
        .allowlist_type("BIO_METHOD")
        .allowlist_type("BUF_MEM");

    for sym in [
        "OSSL_LIB_CTX_new",
        "OSSL_LIB_CTX_free",
        "OSSL_LIB_CTX_load_config",
        "OSSL_PROVIDER_load",
        "OSSL_PROVIDER_unload",
        "OSSL_PROVIDER_available",
    ] {
        b = b.allowlist_item(sym);
    }
    b.allowlist_type("OSSL_LIB_CTX")
        .allowlist_type("OSSL_PROVIDER")
}

fn allow_digest_cipher_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "EVP_MD_fetch",
        "EVP_MD_free",
        "EVP_MD_up_ref",
        "EVP_MD_get_size",
        "EVP_MD_get_block_size",
        "EVP_MD_get_type",
        "EVP_MD_CTX_new",
        "EVP_MD_CTX_free",
        "EVP_MD_CTX_copy_ex",
        "EVP_DigestInit_ex2",
        "EVP_DigestUpdate",
        "EVP_DigestFinal_ex",
        "EVP_DigestFinalXOF",
        "EVP_Digest",
        "EVP_MD_CTX_get0_md",
        // Available in OpenSSL >= 4.0.0; guarded by #[cfg(ossl400)] in native-ossl.
        "EVP_MD_CTX_serialize",
        "EVP_MD_CTX_deserialize",
    ] {
        b = b.allowlist_item(sym);
    }
    b = b.allowlist_type("EVP_MD").allowlist_type("EVP_MD_CTX");

    for sym in [
        "EVP_CIPHER_fetch",
        "EVP_CIPHER_free",
        "EVP_CIPHER_up_ref",
        "EVP_CIPHER_get_key_length",
        "EVP_CIPHER_get_iv_length",
        "EVP_CIPHER_get_block_size",
        "EVP_CIPHER_get_flags",
        "EVP_CIPHER_CTX_new",
        "EVP_CIPHER_CTX_free",
        "EVP_CIPHER_CTX_get_block_size",
        "EVP_EncryptInit_ex2",
        "EVP_EncryptUpdate",
        "EVP_EncryptFinal_ex",
        "EVP_DecryptInit_ex2",
        "EVP_DecryptUpdate",
        "EVP_DecryptFinal_ex",
        "EVP_CIPHER_CTX_set_params",
        "EVP_CIPHER_CTX_get_params",
        "EVP_CIPHER_CTX_ctrl",
        "EVP_CTRL_GCM_GET_TAG",
        "EVP_CTRL_GCM_SET_TAG",
        "EVP_CTRL_CCM_GET_TAG",
        "EVP_CTRL_CCM_SET_TAG",
    ] {
        b = b.allowlist_item(sym);
    }
    b.allowlist_type("EVP_CIPHER")
        .allowlist_type("EVP_CIPHER_CTX")
}

fn allow_mac_rand_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "EVP_MAC_fetch",
        "EVP_MAC_free",
        "EVP_MAC_up_ref",
        "EVP_MAC_CTX_new",
        "EVP_MAC_CTX_free",
        "EVP_MAC_CTX_dup",
        "EVP_MAC_init",
        "EVP_MAC_update",
        "EVP_MAC_final",
        "EVP_MAC_finalXOF",
        "EVP_MAC_CTX_get_mac_size",
        "EVP_MAC_CTX_get_block_size",
        "EVP_MAC_CTX_get0_mac",
        "EVP_MAC_get0_name",
    ] {
        b = b.allowlist_item(sym);
    }
    b = b.allowlist_type("EVP_MAC").allowlist_type("EVP_MAC_CTX");

    for sym in [
        "EVP_RAND_fetch",
        "EVP_RAND_free",
        "EVP_RAND_CTX_new",
        "EVP_RAND_CTX_free",
        "EVP_RAND_CTX_up_ref",
        "EVP_RAND_generate",
        "EVP_RAND_instantiate",
        "EVP_RAND_CTX_set_params",
        "EVP_RAND_get_params",
        "EVP_RAND_get_strength",
        "EVP_RAND_get_state",
        "RAND_bytes",
        "RAND_priv_bytes",
        "RAND_get0_primary",
        "RAND_get0_public",
        "RAND_get0_private",
    ] {
        b = b.allowlist_item(sym);
    }
    b.allowlist_type("EVP_RAND").allowlist_type("EVP_RAND_CTX")
}

fn allow_pkey_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "EVP_PKEY_free",
        "EVP_PKEY_up_ref",
        "EVP_PKEY_get_bits",
        "EVP_PKEY_get_security_bits",
        "EVP_PKEY_get_size",
        "EVP_PKEY_is_a",
        "EVP_PKEY_get_params",
        "EVP_PKEY_eq",
        "EVP_PKEY_get_default_digest_name",
        "d2i_PrivateKey_bio",
        "d2i_PUBKEY_bio",
        "d2i_AutoPrivateKey_ex",
        "d2i_PUBKEY_ex",
        "i2d_PrivateKey",
        "i2d_PUBKEY",
        "PEM_read_bio_PrivateKey",
        "PEM_read_bio_PrivateKey_ex",
        "PEM_write_bio_PrivateKey",
        "PEM_read_bio_PUBKEY",
        "PEM_read_bio_PUBKEY_ex",
        "PEM_write_bio_PUBKEY",
    ] {
        b = b.allowlist_item(sym);
    }
    b = b.allowlist_type("EVP_PKEY");

    for sym in [
        "EVP_PKEY_CTX_free",
        "EVP_PKEY_CTX_new_from_name",
        "EVP_PKEY_CTX_new_from_pkey",
        "EVP_PKEY_CTX_set_params",
        "EVP_PKEY_CTX_get_params",
        "EVP_PKEY_keygen_init",
        "EVP_PKEY_keygen",
        "EVP_PKEY_paramgen_init",
        "EVP_PKEY_paramgen",
        "EVP_PKEY_derive_init",
        "EVP_PKEY_derive_set_peer",
        "EVP_PKEY_derive",
        "EVP_PKEY_encrypt_init",
        "EVP_PKEY_encrypt",
        "EVP_PKEY_decrypt_init",
        "EVP_PKEY_decrypt",
        "EVP_PKEY_encapsulate_init",
        "EVP_PKEY_encapsulate",
        "EVP_PKEY_decapsulate_init",
        "EVP_PKEY_decapsulate",
        "EVP_DigestSignInit_ex",
        "EVP_DigestSignUpdate",
        "EVP_DigestSignFinal",
        "EVP_DigestSign",
        "EVP_DigestVerifyInit_ex",
        "EVP_DigestVerifyUpdate",
        "EVP_DigestVerifyFinal",
        "EVP_DigestVerify",
        "EVP_PKEY_fromdata_init",
        "EVP_PKEY_fromdata",
        "EVP_PKEY_todata",
        // Raw (no-digest) sign / verify.
        "EVP_PKEY_sign_init",
        "EVP_PKEY_verify_init",
        "EVP_PKEY_sign",
        "EVP_PKEY_verify",
        // Message-based sign / verify (OpenSSL 3.2+, always present at our 3.5 floor).
        "EVP_PKEY_sign_message_init",
        "EVP_PKEY_verify_message_init",
        "EVP_PKEY_sign_message_update",
        "EVP_PKEY_verify_message_update",
        "EVP_PKEY_sign_message_final",
        "EVP_PKEY_verify_message_final",
        "EVP_PKEY_CTX_set_signature",
    ] {
        b = b.allowlist_item(sym);
    }
    b.allowlist_type("EVP_PKEY_CTX")
}

fn allow_kdf_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "EVP_KDF_fetch",
        "EVP_KDF_free",
        "EVP_KDF_CTX_new",
        "EVP_KDF_CTX_free",
        "EVP_KDF_derive",
        "EVP_KDF_CTX_set_params",
        "EVP_KDF_CTX_get_params",
        "EVP_KDF_CTX_get_kdf_size",
        "EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND",
        "EVP_KDF_HKDF_MODE_EXTRACT_ONLY",
        "EVP_KDF_HKDF_MODE_EXPAND_ONLY",
    ] {
        b = b.allowlist_item(sym);
    }
    b.allowlist_type("EVP_KDF").allowlist_type("EVP_KDF_CTX")
}

fn allow_bignum_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "BN_bin2bn",
        "BN_bn2bin",
        "BN_bn2nativepad",
        "BN_num_bits",
        "BN_free",
    ] {
        b = b.allowlist_function(sym);
    }
    b.allowlist_type("BIGNUM")
}

fn allow_obj_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "OBJ_txt2obj",
        "OBJ_obj2txt",
        "OBJ_cmp",
        "OBJ_dup",
        "ASN1_OBJECT_free",
    ] {
        b = b.allowlist_function(sym);
    }
    b.allowlist_type("ASN1_OBJECT")
}

fn allow_cms_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        // Lifecycle
        "CMS_ContentInfo_new",
        "CMS_ContentInfo_new_ex",
        "CMS_ContentInfo_free",
        // DER encode / decode
        "d2i_CMS_ContentInfo",
        "i2d_CMS_ContentInfo",
        "d2i_CMS_bio",
        "i2d_CMS_bio",
        // Sign / verify
        "CMS_sign",
        "CMS_sign_ex",
        "CMS_verify",
        "CMS_add1_signer",
        // Content accessors
        "CMS_get0_type",
        "CMS_get0_eContentType",
        "CMS_get0_content",
        "CMS_is_detached",
        // SignerInfo stack
        "CMS_get0_SignerInfos",
        "CMS_SignerInfo_get0_algs",
        "CMS_set1_signers_certs",
        // Certificate / CRL embedding
        "CMS_add0_cert",
        "CMS_add1_cert",
        "CMS_get1_certs",
        "CMS_get1_crls",
    ] {
        b = b.allowlist_function(sym);
    }
    b.allowlist_type("CMS_ContentInfo")
        .allowlist_type("CMS_SignerInfo")
}

fn allow_decoder_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "OSSL_DECODER_CTX_new_for_pkey",
        "OSSL_DECODER_CTX_free",
        "OSSL_DECODER_from_data",
    ] {
        b = b.allowlist_function(sym);
    }
    b.allowlist_type("OSSL_DECODER_CTX")
}

fn allow_signature_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "EVP_SIGNATURE_fetch",
        "EVP_SIGNATURE_free",
        "EVP_SIGNATURE_up_ref",
    ] {
        b = b.allowlist_item(sym);
    }
    b.allowlist_type("EVP_SIGNATURE")
}

fn allow_x509_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "X509_new",
        "X509_new_ex",
        "X509_free",
        "X509_up_ref",
        "d2i_X509",
        "i2d_X509",
        "PEM_read_bio_X509",
        "PEM_write_bio_X509",
        "X509_get_subject_name",
        "X509_get_issuer_name",
        "X509_get0_pubkey",
        "X509_get_pubkey",
        "X509_get0_serialNumber",
        "X509_get_serialNumber",
        "X509_get0_notBefore",
        "X509_get0_notAfter",
        "X509_verify",
        "X509_self_signed",
        "X509_get_ext_count",
        "X509_get_ext",
        "X509_get_ext_by_NID",
        "X509_get_ext_d2i",
        "X509_NAME_entry_count",
        "X509_NAME_get_entry",
        "X509_NAME_oneline",
        "X509_NAME_ENTRY_get_object",
        "X509_NAME_ENTRY_get_data",
        "X509_cmp_time",
        "ASN1_TIME_to_tm",
        "ASN1_INTEGER_get_int64",
        "X509_set_version",
        "X509_set_serialNumber",
        "X509_set_subject_name",
        "X509_set_issuer_name",
        "X509_set_pubkey",
        "X509_sign",
        "X509_NAME_new",
        "X509_NAME_add_entry_by_txt",
        "OBJ_nid2sn",
        "OBJ_nid2ln",
        "OBJ_obj2nid",
        "OBJ_txt2nid",
        "OBJ_sn2nid",
        "X509_EXTENSION_get_object",
        "X509_EXTENSION_get_critical",
        "X509_EXTENSION_get_data",
        "X509_NAME_print_ex",
        "X509_NAME_free",
        "ASN1_STRING_get0_data",
        "ASN1_STRING_length",
        "ASN1_TIME_print",
        "X509_gmtime_adj",
        "X509_getm_notBefore",
        "X509_getm_notAfter",
        "ASN1_INTEGER_new",
        "ASN1_INTEGER_free",
        "ASN1_INTEGER_set_int64",
        "X509_get_signature_info",
        "X509_get_key_usage",
        "X509_get_extended_key_usage",
        "GENERAL_NAME_free",
        "i2d_ASN1_TYPE",
    ] {
        b = b.allowlist_item(sym);
    }
    b.allowlist_type("X509")
        .allowlist_type("X509_NAME")
        .allowlist_type("X509_NAME_ENTRY")
        .allowlist_type("ASN1_TIME")
        .allowlist_type("ASN1_INTEGER")
        .allowlist_type("X509_EXTENSION")
        .allowlist_type("GENERAL_NAME")
        .allowlist_type("GENERAL_NAMES")
        .allowlist_type("ASN1_STRING")
        .allowlist_type("ASN1_OCTET_STRING")
        .allowlist_type("ASN1_TYPE")
        .allowlist_type("OTHERNAME")
}

fn allow_ssl_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    for sym in [
        "SSL_CTX_new",
        "SSL_CTX_free",
        "SSL_CTX_up_ref",
        "SSL_CTX_ctrl",
        "SSL_ctrl",
        "TLS_method",
        "TLS_client_method",
        "TLS_server_method",
        "SSL_CTX_set_cipher_list",
        "SSL_CTX_set_ciphersuites",
        "SSL_CTX_use_certificate",
        "SSL_CTX_use_PrivateKey",
        "SSL_CTX_check_private_key",
        "SSL_CTX_load_verify_locations",
        "SSL_CTX_set_default_verify_paths",
        "SSL_CTX_set_verify",
        "SSL_CTX_set_session_cache_mode",
        // ALPN protocol list (wire-format; 0 on success — inverted convention)
        "SSL_CTX_set_alpn_protos",
        // Per-context certificate store (borrowed pointer, do not free)
        "SSL_CTX_get_cert_store",
        // Per-context verify params (borrowed; set hostname for cert verification)
        "SSL_CTX_get0_param",
        // Per-connection hostname verification (SSL_set1_host)
        "SSL_set1_host",
        "SSL_new",
        "SSL_free",
        "SSL_set_bio",
        "SSL_set_connect_state",
        "SSL_set_accept_state",
        "SSL_connect",
        "SSL_accept",
        "SSL_do_handshake",
        "SSL_read_ex",
        "SSL_write_ex",
        "SSL_shutdown",
        "SSL_get_error",
        "SSL_get0_peer_certificate",
        "SSL_get_peer_cert_chain",
        "SSL_get_session",
        "SSL_get1_session",
        "SSL_set_session",
        "SSL_SESSION_up_ref",
        "SSL_SESSION_free",
        // SSL_VERIFY_* constants for verify mode flags
        "SSL_VERIFY_PEER",
        "SSL_VERIFY_FAIL_IF_NO_PEER_CERT",
        // SSL_CTRL_* numeric codes used in SSL_CTX_ctrl calls
        "SSL_CTRL_EXTRA_CHAIN_CERT",
        "SSL_CTRL_SET_MIN_PROTO_VERSION",
        "SSL_CTRL_SET_MAX_PROTO_VERSION",
        "SSL_CTRL_SET_SESS_CACHE_MODE",
    ] {
        b = b.allowlist_item(sym);
    }
    // X509_VERIFY_PARAM — for context-level hostname verification
    for sym in [
        "X509_VERIFY_PARAM_set1_host",
        "X509_VERIFY_PARAM_set_hostflags",
    ] {
        b = b.allowlist_function(sym);
    }
    b.allowlist_type("SSL_CTX")
        .allowlist_type("SSL")
        .allowlist_type("SSL_SESSION")
        .allowlist_type("SSL_METHOD")
        .allowlist_type("X509_VERIFY_PARAM")
}

fn allow_x509_advanced_syms(mut b: bindgen::Builder) -> bindgen::Builder {
    // OPENSSL_STACK — raw stack primitives needed to iterate STACK_OF(X509).
    for sym in [
        "OPENSSL_sk_num",
        "OPENSSL_sk_value",
        "OPENSSL_sk_pop_free",
        "OPENSSL_sk_free",
        "OPENSSL_sk_new_null",
        "OPENSSL_sk_push",
    ] {
        b = b.allowlist_function(sym);
    }
    b = b.allowlist_type("OPENSSL_STACK");

    // X509_STORE — trust store
    for sym in [
        "X509_STORE_new",
        "X509_STORE_free",
        "X509_STORE_up_ref",
        "X509_STORE_add_cert",
        "X509_STORE_add_crl",
        "X509_STORE_set_flags",
        "X509_STORE_CTX_new",
        "X509_STORE_CTX_free",
        "X509_STORE_CTX_init",
        "X509_STORE_CTX_get_error",
        "X509_STORE_CTX_get0_chain",
        "X509_verify_cert",
    ] {
        b = b.allowlist_item(sym);
    }
    b = b
        .allowlist_type("X509_STORE")
        .allowlist_type("X509_STORE_CTX");

    // X509_CRL — certificate revocation list
    for sym in [
        "X509_CRL_new",
        "X509_CRL_new_ex",
        "X509_CRL_free",
        "X509_CRL_up_ref",
        "PEM_read_bio_X509_CRL",
        "PEM_write_bio_X509_CRL",
        "d2i_X509_CRL_bio",
        "i2d_X509_CRL_bio",
        "X509_CRL_get_issuer",
        "X509_CRL_get0_lastUpdate",
        "X509_CRL_get0_nextUpdate",
        "X509_CRL_verify",
    ] {
        b = b.allowlist_item(sym);
    }
    b = b.allowlist_type("X509_CRL");

    // PKCS12 — PFX bundles
    for sym in [
        "PKCS12_new",
        "PKCS12_free",
        "d2i_PKCS12_bio",
        "i2d_PKCS12_bio",
        "PKCS12_parse",
        "PKCS12_create_ex",
    ] {
        b = b.allowlist_item(sym);
    }
    b = b.allowlist_type("PKCS12");

    // OCSP — certificate status
    for sym in [
        "OCSP_REQUEST_new",
        "OCSP_REQUEST_free",
        "d2i_OCSP_REQUEST",
        "i2d_OCSP_REQUEST",
        "OCSP_RESPONSE_new",
        "OCSP_RESPONSE_free",
        "d2i_OCSP_RESPONSE",
        "i2d_OCSP_RESPONSE",
        "OCSP_response_status",
        "OCSP_response_get1_basic",
        "OCSP_BASICRESP_new",
        "OCSP_BASICRESP_free",
        "OCSP_basic_verify",
        // SingleResponse / CERTID layer
        "OCSP_CERTID_new",
        "OCSP_CERTID_free",
        "OCSP_CERTID_dup",
        "OCSP_cert_to_id",
        "OCSP_request_add0_id",
        "OCSP_resp_count",
        "OCSP_resp_get0",
        "OCSP_resp_find",
        "OCSP_resp_find_status",
        "OCSP_single_get0_status",
        "OCSP_check_validity",
    ] {
        b = b.allowlist_item(sym);
    }
    b.allowlist_type("OCSP_REQUEST")
        .allowlist_type("OCSP_RESPONSE")
        .allowlist_type("OCSP_BASICRESP")
        .allowlist_type("OCSP_CERTID")
        .allowlist_type("OCSP_SINGLERESP")
}

// ── OpenSSL discovery ────────────────────────────────────────────────────────

/// Paths to the OpenSSL installation used for compilation and linking.
struct OsslPaths {
    /// Directories to pass as `-I` clang args (headers).
    include_paths: Vec<std::path::PathBuf>,
}

/// Discover the OpenSSL installation.
///
/// When `NATIVE_OSSL_OPENSSL_SOURCES` or `KRYOPTIC_OPENSSL_SOURCES` is set it
/// must point to a directory that was produced by building OpenSSL in-tree.
/// `NATIVE_OSSL_OPENSSL_SOURCES` is checked first; `KRYOPTIC_OPENSSL_SOURCES`
/// is a convenience alias for builds that already export that variable.
/// The function expects:
///
/// * `$NATIVE_OSSL_OPENSSL_SOURCES/include/` — public OpenSSL headers
/// * `$NATIVE_OSSL_OPENSSL_SOURCES/libcrypto.a` — static crypto library
///
/// When the variable is absent the function falls back to `pkg-config`.
fn discover_openssl() -> OsslPaths {
    println!("cargo:rerun-if-env-changed=NATIVE_OSSL_OPENSSL_SOURCES");
    println!("cargo:rerun-if-env-changed=KRYOPTIC_OPENSSL_SOURCES");

    if let Ok(sources) = std::env::var("NATIVE_OSSL_OPENSSL_SOURCES") {
        let base = std::path::PathBuf::from(&sources)
            .canonicalize()
            .unwrap_or_else(|e| {
                panic!("NATIVE_OSSL_OPENSSL_SOURCES={sources}: cannot canonicalize: {e}")
            });
        let include = base.join("include");
        assert!(
            include.exists(),
            "NATIVE_OSSL_OPENSSL_SOURCES: no include/ directory at {}",
            include.display()
        );

        println!("cargo:rustc-link-search=native={}", base.display());
        println!("cargo:rustc-link-lib=static=crypto");
        println!("cargo:rerun-if-changed={}/libcrypto.a", base.display());

        OsslPaths {
            include_paths: vec![include],
        }
    } else if let Ok(sources) = std::env::var("KRYOPTIC_OPENSSL_SOURCES") {
        let base = std::path::PathBuf::from(&sources)
            .canonicalize()
            .unwrap_or_else(|e| {
                panic!("KRYOPTIC_OPENSSL_SOURCES={sources}: cannot canonicalize: {e}")
            });
        let include = base.join("include");
        assert!(
            include.exists(),
            "KRYOPTIC_OPENSSL_SOURCES: no include/ directory at {}",
            include.display()
        );

        println!("cargo:rustc-link-search=native={}", base.display());
        println!("cargo:rustc-link-lib=static=crypto");
        println!("cargo:rerun-if-changed={}/libcrypto.a", base.display());

        OsslPaths {
            include_paths: vec![include],
        }
    } else {
        let lib = pkg_config::Config::new()
            .atleast_version("3.0.7")
            .probe("openssl")
            .expect("openssl >= 3.0.7 not found via pkg-config");

        OsslPaths {
            include_paths: lib.include_paths,
        }
    }
}

// ── fips-provider second bindgen pass ────────────────────────────────────────

/// Compiles a small C probe to determine the byte offset of `keydata` inside
/// `evp_pkey_st` on the current (or target) ABI, then writes
/// `$OUT_DIR/keydata_offset.rs` containing a single Rust constant:
///
/// ```rust,ignore
/// pub const EVP_PKEY_KEYDATA_OFFSET: usize = <value>;
/// ```
///
/// Using the C compiler's `offsetof` guarantees correctness across all
/// supported 64-bit LP64 targets (`x86_64`, aarch64, riscv64, s390x, …).
/// The constant is exposed through `native_ossl_sys::fips_internal` and used
/// in `Pkey::keydata()` instead of a hardcoded literal.
///
/// Cross-compilation note: the probe is compiled for the **host** (build
/// machine) when `HOST != TARGET`.  This is correct on LP64 targets where the
/// struct layout is the same regardless of CPU ISA.  For genuinely different
/// layouts (e.g. 32-bit targets), extend the table inside this function.
fn compute_keydata_offset(paths: &OsslPaths, source_include: &Path, out_dir: &Path) {
    let _host = std::env::var("HOST").unwrap_or_default();
    let target = std::env::var("TARGET").unwrap_or_default();
    let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
    let target_pointer_width = std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap_or_default();

    // For 32-bit targets the struct layout differs (pointers are 4 bytes).
    // The fips-provider feature is not yet validated on 32-bit; bail early with
    // a clear message rather than silently using a wrong offset.
    assert!(
        target_pointer_width != "32",
        "fips-provider is not yet ported to 32-bit target {target}.\n\
         The evp_pkey_st::keydata offset is architecture-dependent and\n\
         has not been verified on 32-bit platforms."
    );

    let probe_src = out_dir.join("keydata_probe.c");
    // The probe mirrors the same #define guards as fips_wrapper.h so it sees
    // exactly the same struct layout as the rest of the fips-provider build.
    std::fs::write(
        &probe_src,
        b"#include <stdio.h>\n\
          #include <stddef.h>\n\
          #define OSSL_INTERNAL_REFCOUNT_H\n\
          typedef struct { int val; } CRYPTO_REF_COUNT;\n\
          #include <openssl/evp.h>\n\
          #include <crypto/evp.h>\n\
          int main(void) {\n\
              printf(\"%zu\\n\", offsetof(struct evp_pkey_st, keydata));\n\
              return 0;\n\
          }\n",
    )
    .expect("failed to write keydata_probe.c");

    let probe_bin = out_dir.join("keydata_probe");

    // Prefer the cross-aware C compiler env var, fall back to the host `cc`.
    // For most cross-compilation scenarios targeting another LP64 Linux ABI the
    // host offset matches the target offset (same struct layout rules), so
    // compiling for the host gives the correct answer.
    let cc_var = format!("CC_{}", target.replace('-', "_"));
    let cc = std::env::var(&cc_var)
        .or_else(|_| std::env::var("CC"))
        .unwrap_or_else(|_| "cc".to_string());

    let mut cmd = std::process::Command::new(&cc);
    for p in &paths.include_paths {
        cmd.arg(format!("-I{}", p.display()));
    }
    cmd.arg(format!("-I{}", source_include.display()));
    cmd.arg(&probe_src).arg("-o").arg(&probe_bin);

    let status = cmd
        .status()
        .unwrap_or_else(|e| panic!("failed to run C compiler '{cc}': {e}"));
    assert!(
        status.success(),
        "keydata_probe compilation failed (compiler: {cc})\n\
         Ensure {cc} can access the OpenSSL headers."
    );

    // Compile was for the host; on a cross-compilation environment the binary
    // may not be executable.  If running fails, fall back to the known value for
    // the LP64 x86_64/aarch64/riscv64 struct layout.
    let offset: usize = std::process::Command::new(&probe_bin)
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .and_then(|s| s.trim().parse().ok())
        .unwrap_or_else(|| {
            // Cross-compilation fallback: use architecture-known offsets.
            match target_arch.as_str() {
                // All 64-bit LP64 Linux targets with the same non-FIPS struct layout
                // produce offset 88 for the non-FIPS (system libcrypto) evp_pkey_st.
                "x86_64" | "aarch64" | "riscv64" | "s390x" | "powerpc64" | "mips64" => 88,
                _ => panic!(
                    "Could not determine evp_pkey_st::keydata offset for target \
                     '{target_arch}'.  The fips-provider feature requires either a \
                     native build (probe binary can run) or a known-architecture \
                     target.  Add the offset to the table in native-ossl-sys/build.rs \
                     or disable fips-provider for this target."
                ),
            }
        });

    let offset_rs = out_dir.join("keydata_offset.rs");
    std::fs::write(
        &offset_rs,
        format!(
            "/// Byte offset of the `keydata` field in `evp_pkey_st`,\n\
             /// computed by the C compiler for this target ABI.\n\
             /// Used by [`Pkey::keydata`](crate::pkey::Pkey::keydata).\n\
             pub const EVP_PKEY_KEYDATA_OFFSET: usize = {offset};\n"
        ),
    )
    .expect("failed to write keydata_offset.rs");
}

/// Generates `fips_bindings.rs` by including non-public OpenSSL headers that
/// expose the `EVP_SIGNATURE` vtable struct, `evp_pkey_st` internals, `PROV_CTX`
/// helpers, and `ossl_prov_*` functions required by a FIPS provider.
///
/// Requires the `OPENSSL_SOURCE_DIR` environment variable to point to the root
/// of the OpenSSL source tree (the directory containing `crypto/`, `include/`,
/// and `providers/`).  The installed public headers (from pkg-config) are still
/// used for types like `OSSL_PARAM` and `BIO`.
fn build_fips_bindings(paths: &OsslPaths, out_dir: &Path) {
    let source_dir = std::env::var("OPENSSL_SOURCE_DIR").unwrap_or_else(|_| {
        panic!(
            "OPENSSL_SOURCE_DIR must be set when building with the fips-provider feature.\n\
             Point it at the root of the OpenSSL source tree, e.g.:\n\
             OPENSSL_SOURCE_DIR=/path/to/openssl cargo build --features fips-provider"
        )
    });
    println!("cargo:rerun-if-env-changed=OPENSSL_SOURCE_DIR");

    let source_path = std::path::PathBuf::from(&source_dir);
    // source_path/include contains crypto/evp.h, internal/provider.h, etc.
    let source_include = source_path.join("include");
    // source_path/crypto/evp contains evp_local.h
    let evp_local_dir = source_path.join("crypto").join("evp");
    // source_path/providers/common/include contains prov/providercommon.h and prov/provider_ctx.h
    let prov_common_include = source_path.join("providers").join("common").join("include");

    let fips_bindings = bindgen::Builder::default()
        .header_contents("fips_wrapper.h", FIPS_WRAPPER_H)
        .layout_tests(false)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        // System installed OpenSSL includes (for public types like OSSL_PARAM).
        .clang_args(
            paths
                .include_paths
                .iter()
                .map(|p| format!("-I{}", p.display())),
        )
        // OpenSSL source tree include directories.
        .clang_arg(format!("-I{}", source_include.display()))
        .clang_arg(format!("-I{}", evp_local_dir.display()))
        .clang_arg(format!("-I{}", prov_common_include.display()))
        // evp_signature_st and evp_pkey_st — currently opaque in the generated bindings.
        //
        // Root cause: OSSL_FUNC_*_fn typedefs in <openssl/core_dispatch.h> use the K&R
        // function-type form `typedef T (fn_t)(args)`.  bindgen 0.71.1 cannot translate
        // K&R function-type typedefs to Rust; struct fields that reference these types
        // are untranslatable, so the whole struct becomes an opaque stub.
        //
        // The hand-written EvpSignatureVtable in fips.rs is the correct workaround:
        // it declares only the fields we actually call, with concrete Rust fn pointers.
        // Once bindgen properly handles K&R function-type typedefs, these allowlist
        // entries will enable automatic generation and EvpSignatureVtable can be dropped.
        .allowlist_type("evp_signature_st")
        .allowlist_type("evp_pkey_st")
        // PROV_CTX struct (from prov/provider_ctx.h).
        .allowlist_type("prov_ctx_st")
        // Internal provider lifecycle and state helpers.
        .allowlist_function("ossl_prov_is_running")
        .allowlist_function("ossl_set_error_state")
        .allowlist_function("ossl_prov_ctx_new")
        .allowlist_function("ossl_prov_ctx_free")
        .allowlist_function("ossl_prov_ctx_set0_handle")
        .allowlist_function("ossl_prov_ctx_set0_libctx")
        .allowlist_function("ossl_prov_ctx_get0_libctx")
        .allowlist_function("ossl_prov_ctx_get0_handle")
        // FIPS self-test constant (from openssl/self_test.h).
        .allowlist_var("OSSL_SELF_TEST_TYPE_PCT")
        // FIPS provider entry point (only meaningful for FIPS provider builds).
        .allowlist_function("OSSL_provider_init_int")
        .generate()
        .expect("bindgen failed to generate fips bindings");

    fips_bindings
        .write_to_file(out_dir.join("fips_bindings.rs"))
        .expect("failed to write fips_bindings.rs");

    // Compute the byte offset of evp_pkey_st::keydata via a small C probe so
    // that Pkey::keydata() is correct on all supported 64-bit LP64 targets.
    compute_keydata_offset(paths, &source_include, out_dir);
}

// ── Wrapper header contents ───────────────────────────────────────────────────

/// Public OpenSSL headers pulled in for the main bindgen pass.
///
/// Passed via `header_contents` (in-memory) rather than written to `$OUT_DIR`
/// as a real file.  Writing a synthesised header to disk and using it as a
/// `cargo:rerun-if-changed` input causes Cargo to mark the crate dirty on
/// every build: the file's mtime is always newer than the fingerprint recorded
/// at the end of the previous build.
/// Non-public OpenSSL headers for the `fips-provider` feature bindgen pass.
///
/// See `WRAPPER_H` for why this is passed via `header_contents` rather than
/// written to disk.
const FIPS_WRAPPER_H: &str = "/* Non-public OpenSSL headers for fips-provider feature */\n\
     /* Intentionally NOT defining FIPS_MODULE: native-ossl wraps the */\n\
     /* system libcrypto, so EVP_PKEY structs use the full (non-FIPS) layout. */\n\
     \n\
     /*\n\
      * Pre-empt <internal/refcount.h> to strip _Atomic from CRYPTO_REF_COUNT.\n\
      *\n\
      * WHY: evp_signature_st and evp_pkey_st embed CRYPTO_REF_COUNT by value.\n\
      * On C11-atomics platforms, CRYPTO_REF_COUNT is `struct { _Atomic int val; }`.\n\
      * The CURRENT reason these structs are still opaque in the generated bindings\n\
      * is NOT _Atomic -- it is that OSSL_FUNC_*_fn typedefs in core_dispatch.h\n\
      * use the K&R function-type form (`typedef T (fn_t)(args)`) rather than the\n\
      * pointer-to-function form (`typedef T (*fn_t)(args)`), and bindgen 0.71.1\n\
      * cannot translate K&R function-type typedefs to Rust; any struct with fields\n\
      * of those types therefore becomes opaque.\n\
      *\n\
      * FUTURE-PROOFING: If a future bindgen release fixes the K&R typedef issue,\n\
      * _Atomic in CRYPTO_REF_COUNT would become the *next* blocker.  We pre-empt\n\
      * refcount.h now so that fix becomes the only remaining step:\n\
      *   - Claim refcount.h's guard (OSSL_INTERNAL_REFCOUNT_H) to skip the header.\n\
      *   - Define CRYPTO_REF_COUNT with plain `int val` (no _Atomic).\n\
      *   - On platforms where ATOMIC_INT_LOCK_FREE > 0 (verified below with\n\
      *     _Static_assert), `_Atomic int` and `int` are layout-identical, so\n\
      *     the generated struct layouts remain byte-accurate on all such targets.\n\
      *\n\
      * This wrapper is ONLY consumed by bindgen, never compiled into real code,\n\
      * so the stripped atomic qualifier has no runtime consequence.\n\
      */\n\
     /* Assert the layout assumption before cloaking refcount.h. */\n\
     #include <stdatomic.h>\n\
     #if defined(ATOMIC_INT_LOCK_FREE) && ATOMIC_INT_LOCK_FREE > 0\n\
     _Static_assert(sizeof(_Atomic int) == sizeof(int),\n\
         \"fips_wrapper: _Atomic int and int differ in size on this target\");\n\
     _Static_assert(_Alignof(_Atomic int) == _Alignof(int),\n\
         \"fips_wrapper: _Atomic int and int differ in alignment on this target\");\n\
     #endif\n\
     \n\
     #define OSSL_INTERNAL_REFCOUNT_H\n\
     typedef struct { int val; } CRYPTO_REF_COUNT;\n\
     \n\
     #include <openssl/evp.h>\n\
     #include <crypto/evp.h>\n\
     #include <openssl/fips_names.h>\n\
     #include <internal/provider.h>\n\
     #include <evp_local.h>\n\
     #include <prov/providercommon.h>\n\
     #include <prov/provider_ctx.h>\n\
     #include <openssl/self_test.h>\n\
     \n\
     int OSSL_provider_init_int(\n\
         const OSSL_CORE_HANDLE *handle,\n\
         const OSSL_DISPATCH *in,\n\
         const OSSL_DISPATCH **out,\n\
         void **provctx);\n";

const WRAPPER_H: &str = "#include <openssl/evp.h>\n\
    #include <openssl/bio.h>\n\
    #include <openssl/x509.h>\n\
    #include <openssl/x509_vfy.h>\n\
    #include <openssl/ssl.h>\n\
    #include <openssl/rand.h>\n\
    #include <openssl/kdf.h>\n\
    #include <openssl/err.h>\n\
    #include <openssl/param_build.h>\n\
    #include <openssl/core_names.h>\n\
    #include <openssl/crypto.h>\n\
    #include <openssl/provider.h>\n\
    #include <openssl/pkcs12.h>\n\
    #include <openssl/ocsp.h>\n\
    #include <openssl/stack.h>\n\
    #include <openssl/decoder.h>\n\
    #include <openssl/cms.h>\n";

// ── Entry point ───────────────────────────────────────────────────────────────

fn main() {
    let paths = discover_openssl();

    for path in &paths.include_paths {
        println!("cargo:include={}", path.display());
    }
    println!("cargo:rerun-if-changed=build.rs");
    // Declare all cfg flags emitted by OsslCallbacks so rustc doesn't warn on them.
    println!("cargo::rustc-check-cfg=cfg(ossl307,ossl310,ossl320,ossl350,ossl_v400,ossl_slhdsa,ossl_mldsa,ossl_mlkem,param_clear_free)");

    let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set"));

    // When the fips-provider feature is active, perform a second bindgen pass
    // against the non-public OpenSSL headers to expose EVP_SIGNATURE vtable
    // internals, PROV_CTX helpers, and ossl_prov_* functions needed by a FIPS
    // provider implementation.
    if std::env::var("CARGO_FEATURE_FIPS_PROVIDER").is_ok() {
        build_fips_bindings(&paths, &out_dir);
    }

    let mut builder = bindgen::Builder::default()
        .header_contents("wrapper.h", WRAPPER_H)
        .parse_callbacks(Box::new(OsslCallbacks))
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .layout_tests(false)
        .clang_args(
            paths
                .include_paths
                .iter()
                .map(|p| format!("-I{}", p.display())),
        );

    builder = allow_error_param_syms(builder);
    builder = allow_bio_libctx_syms(builder);
    builder = allow_digest_cipher_syms(builder);
    builder = allow_mac_rand_syms(builder);
    builder = allow_pkey_syms(builder);
    builder = allow_bignum_syms(builder);
    builder = allow_obj_syms(builder);
    builder = allow_cms_syms(builder);
    builder = allow_decoder_syms(builder);
    builder = allow_signature_syms(builder);
    builder = allow_kdf_syms(builder);
    builder = allow_x509_syms(builder);
    builder = allow_x509_advanced_syms(builder);
    builder = allow_ssl_syms(builder);

    let bindings = builder
        .generate()
        .expect("bindgen failed to generate bindings");
    bindings
        .write_to_file(out_dir.join("ossl_bindings.rs"))
        .expect("failed to write ossl_bindings.rs");
}
