//! CMS SignedData message signing and verification.
//!
//! Produces PEM-encoded PKCS#7/CMS SignedData for text messages.
//! Uses OpenSSL for CMS operations (widely compatible).

use std::io::BufReader;

use base64::Engine;
use cms::content_info::ContentInfo;
use der::{Decode, Encode};
use openssl::cms::CmsContentInfo;
use openssl::pkey::PKey;
use openssl::stack::Stack;
use openssl::x509::X509;
use rustls_pemfile::certs;
use sha2::{Digest, Sha256};
use x509_parser::parse_x509_certificate;

/// Sign a message with the given private key and certificate chain.
/// Returns PEM-encoded CMS SignedData.
pub fn sign_message(
    message: &str,
    private_key_pem: &[u8],
    certificate_chain_pem: &[u8],
) -> Result<String, String> {
    let pkey = PKey::private_key_from_pem(private_key_pem)
        .map_err(|e| format!("parse private key: {e}"))?;

    let chain_str = std::str::from_utf8(certificate_chain_pem)
        .map_err(|_| "certificate chain is not valid UTF-8".to_string())?;
    let mut reader = BufReader::new(chain_str.as_bytes());
    let cert_ders: Vec<Vec<u8>> = certs(&mut reader)
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| format!("parse certificate chain: {e}"))?
        .into_iter()
        .map(|c| c.to_vec())
        .collect();

    let sign_cert = X509::from_der(cert_ders.first().ok_or("certificate chain is empty")?)
        .map_err(|e| format!("parse sign cert: {e}"))?;

    let mut cert_stack = Stack::new().map_err(|e| format!("create cert stack: {e}"))?;
    for der in cert_ders.iter().skip(1) {
        let cert = X509::from_der(der).map_err(|e| format!("parse cert: {e}"))?;
        cert_stack.push(cert).map_err(|e| format!("push cert: {e}"))?;
    }
    let certs_opt = if cert_stack.is_empty() {
        None
    } else {
        Some(cert_stack.as_ref())
    };

    let data = message.as_bytes();
    let cms = CmsContentInfo::sign(
        Some(&sign_cert),
        Some(&pkey),
        certs_opt,
        Some(data),
        openssl::cms::CMSOptions::empty(),
    )
    .map_err(|e| format!("CMS sign: {e}"))?;

    let pem = cms.to_pem().map_err(|e| format!("CMS to_pem: {e}"))?;
    let pem_str = String::from_utf8(pem).map_err(|_| "CMS PEM is not valid UTF-8".to_string())?;
    Ok(pem_str)
}

/// Verify a PEM-encoded CMS SignedData message.
/// Returns (message, signer_common_name, signer_organization, signer_fingerprint, certificate_expired).
pub fn verify_signed_message(
    pem: &str,
) -> Result<
    (
        String,
        Option<String>,
        Option<String>,
        String,
        bool,
    ),
    String,
> {
    let mut cms = CmsContentInfo::from_pem(pem.as_bytes()).map_err(|e| format!("parse CMS: {e}"))?;

    let mut output = Vec::new();
    // NO_SIGNER_CERT_VERIFY: verify the signature cryptographically but skip chain validation.
    // Self-signed and charm.farm certs don't have a full chain to a root; we still want verify to succeed.
    cms.verify(
        None,
        None,
        None,
        Some(&mut output),
        openssl::cms::CMSOptions::NO_SIGNER_CERT_VERIFY,
    )
        .map_err(|e| format!("signature verification failed: {e}"))?;

    let message =
        String::from_utf8(output).map_err(|_| "signed content is not valid UTF-8 text".to_string())?;

    // Extract signer cert from CMS for metadata
    let signer_cert_der = extract_signer_cert_der_from_cms(&cms)?;
    let (cn, org, fingerprint, expired) = cert_metadata_and_fingerprint(&signer_cert_der)?;

    Ok((message, cn, org, fingerprint, expired))
}

/// Extract certificate PEM from SignedData for adding as contact.
pub fn extract_signer_certificate_pem(pem: &str) -> Result<String, String> {
    let cms = CmsContentInfo::from_pem(pem.as_bytes()).map_err(|e| format!("parse CMS: {e}"))?;
    let signer_cert_der = extract_signer_cert_der_from_cms(&cms)?;

    let b64 = base64::engine::general_purpose::STANDARD.encode(&signer_cert_der);
    let chunked: String = b64
        .as_bytes()
        .chunks(64)
        .map(|c| String::from_utf8_lossy(c).into_owned())
        .collect::<Vec<_>>()
        .join("\n");
    let cert_pem = format!(
        "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
        chunked
    );
    Ok(cert_pem)
}

fn extract_signer_cert_der_from_cms(cms: &CmsContentInfo) -> Result<Vec<u8>, String> {
    // CmsContentInfo doesn't expose certs directly; parse DER with cms crate to extract.
    // SignedData. OpenSSL's CMS API doesn't expose a simple "get signer cert" - we can get
    // the certs from the CMS structure. Use the low-level CMS_get0_signers or iterate certs.
    // The openssl crate's CmsContentInfoRef has as_ref() - let me check what's available.

    // Alternative: parse the DER we get from to_der and extract, or use the certs from
    // the verify flow. Actually verify() populates output with the content but doesn't give
    // us the signer cert. We need to extract it from the CMS structure.

    // CmsContentInfo has .0 which is the inner type. Let me check - we might need to use
    // the CmsContentInfoRef and CMS_get1_certs or similar. The openssl crate might not
    // expose that. Let me try a different approach: parse the PEM as DER and parse the
    // structure manually to get the cert from the SignedData certificates field.

    let der = cms.to_der().map_err(|e| format!("to_der: {e}"))?;
    extract_signer_cert_from_cms_der(&der)
}

fn extract_signer_cert_from_cms_der(der: &[u8]) -> Result<Vec<u8>, String> {
    // Parse ContentInfo structure: SEQUENCE { contentType OID, content [0] EXPLICIT ... }
    // For signedData, content is SignedData. SignedData has certificates [0] IMPLICIT.
    // We need to find the first cert in the set (or match by signer info).

    // Use a simple approach: parse with asn1 or similar. The openssl crate doesn't expose
    // CMS_get0_SignedData. Let me use the cms crate just for parsing - we can add it back
    // without the builder feature for verification-only.

    // Simpler: use x509-parser or asn1 to parse. Actually the cryptographic-message-syntax
    // crate can parse. Let me add it for verification only - it has parse_ber and we can
    // get certs.

    // Alternative: use the `bcder` crate which cryptographic-message-syntax uses. Or
    // use the `cms` crate without builder - it can decode ContentInfo and we extract certs.

    // Simplest: use the cms crate's decode (no builder) only for parsing. Let me add cms
    // back with just "pem" feature, no "builder" - that gives us ContentInfo::from_der
    // and we can extract certs.

    // For now, let's use a minimal ASN.1 parse. The CMS SignedData structure is:
    // ContentInfo ::= SEQUENCE { contentType OBJECT IDENTIFIER, content [0] EXPLICIT ANY }
    // For id-signedData (1.2.840.113549.1.7.2), content is SignedData.
    // SignedData ::= SEQUENCE {
    //   version CMSVersion,
    //   digestAlgorithms DigestAlgorithmIdentifiers,
    //   encapContentInfo EncapsulatedContentInfo,
    //   certificates [0] IMPLICIT CertificateSet OPTIONAL,
    //   ...
    // }
    // CertificateSet is a SET OF Certificate. We want the first (leaf) cert.

    // Use the `der` crate or manual parsing. The `cms` crate's ContentInfo from_der
    // would work. Let me add cms with default features only (no builder) for parsing.
    // Actually cms 0.2.3 has ContentInfo in the main crate. Let me add it.

    use cms::signed_data::SignedData;

    let ci = ContentInfo::from_der(der).map_err(|e| format!("parse ContentInfo: {e}"))?;
    let signed_data_der = ci.content.to_der().map_err(|e| format!("content to_der: {e}"))?;
    let sd = SignedData::from_der(signed_data_der.as_slice())
        .map_err(|e| format!("parse SignedData: {e}"))?;
    let signer_info = sd
        .signer_infos
        .0
        .iter()
        .next()
        .ok_or_else(|| "no signer info in signed data".to_string())?;

    let ias = match &signer_info.sid {
        cms::signed_data::SignerIdentifier::IssuerAndSerialNumber(ias) => ias,
        _ => return Err("subjectKeyIdentifier signer not supported".to_string()),
    };

    let certs = sd
        .certificates
        .as_ref()
        .ok_or_else(|| "no certificates in signed data".to_string())?;

    for cert_choice in certs.0.iter() {
        if let cms::cert::CertificateChoices::Certificate(cert) = cert_choice {
            if cert.tbs_certificate.issuer == ias.issuer
                && cert.tbs_certificate.serial_number == ias.serial_number
            {
                return cert.to_der().map_err(|e| format!("cert to_der: {e}"));
            }
        }
    }

    Err("signer certificate not found in signed data".to_string())
}

fn cert_metadata_and_fingerprint(
    cert_der: &[u8],
) -> Result<(Option<String>, Option<String>, String, bool), String> {
    let (_, cert) = parse_x509_certificate(cert_der)
        .map_err(|_| "unable to parse certificate".to_string())?;

    let cn = cert
        .subject()
        .iter_common_name()
        .next()
        .and_then(|v| v.as_str().ok())
        .map(str::to_string);

    let org = cert
        .subject()
        .iter_organization()
        .next()
        .and_then(|v| v.as_str().ok())
        .map(str::to_string);

    let fingerprint = Sha256::digest(cert_der);
    let fp_hex = hex::encode(fingerprint);

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;
    let expired = cert.validity().not_after.timestamp() < now;

    Ok((cn, org, fp_hex, expired))
}
