use std::net::{Ipv4Addr, Ipv6Addr};
use std::path::{Path, PathBuf};

use rpki::data_model::aspa::AspaObject;
use rpki::data_model::crl::RpkixCrl;
use rpki::data_model::manifest::ManifestObject;
use rpki::data_model::rc::{
    AccessDescription, RcExtensions, ResourceCertificate, SubjectInfoAccess,
};
use rpki::data_model::roa::{IpPrefix as RoaIpPrefix, RoaAfi, RoaObject};
use rpki::data_model::signed_object::{
    ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, SignerInfoProfiled,
};
use rpki::data_model::ta::TaCertificate;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ObjectType {
    Auto,
    Cer,
    Mft,
    Crl,
    Roa,
    Aspa,
}

#[derive(Debug, PartialEq, Eq)]
struct Args {
    object_type: ObjectType,
    input_path: Option<PathBuf>,
    out_path: Option<PathBuf>,
    pretty: bool,
    entry_limit: usize,
}

impl Default for Args {
    fn default() -> Self {
        Self {
            object_type: ObjectType::Auto,
            input_path: None,
            out_path: None,
            pretty: true,
            entry_limit: 50,
        }
    }
}

fn usage() -> &'static str {
    "Usage: rpki_object_parse --type auto|cer|mft|crl|roa|asa|aspa --input <object-file> [--out <parsed.json>] [--entry-limit <n|all>] [--compact]"
}

fn main() {
    if let Err(err) = real_main() {
        eprintln!("{err}");
        std::process::exit(1);
    }
}

fn real_main() -> Result<(), String> {
    let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
    let input_path = args.input_path.as_ref().expect("validated");
    let bytes = std::fs::read(input_path)
        .map_err(|e| format!("read input failed: {}: {e}", input_path.display()))?;
    let object_type = resolve_object_type(args.object_type, input_path)?;
    let parsed = parse_object_json(object_type, input_path, &bytes, args.entry_limit);
    let rendered = if args.pretty {
        serde_json::to_string_pretty(&parsed).map_err(|e| e.to_string())?
    } else {
        serde_json::to_string(&parsed).map_err(|e| e.to_string())?
    };

    if let Some(out_path) = args.out_path.as_ref() {
        if let Some(parent) = out_path.parent() {
            std::fs::create_dir_all(parent)
                .map_err(|e| format!("create output parent failed: {}: {e}", parent.display()))?;
        }
        std::fs::write(out_path, rendered)
            .map_err(|e| format!("write output failed: {}: {e}", out_path.display()))?;
    } else {
        println!("{rendered}");
    }
    Ok(())
}

fn parse_args(argv: &[String]) -> Result<Args, String> {
    let mut args = Args::default();
    let mut index = 1usize;
    while index < argv.len() {
        match argv[index].as_str() {
            "--type" => {
                index += 1;
                let value = argv.get(index).ok_or("--type requires a value")?;
                args.object_type = parse_object_type(value)?;
            }
            "--input" | "--in" => {
                index += 1;
                args.input_path = Some(PathBuf::from(
                    argv.get(index).ok_or("--input requires a value")?,
                ));
            }
            "--out" => {
                index += 1;
                args.out_path = Some(PathBuf::from(
                    argv.get(index).ok_or("--out requires a value")?,
                ));
            }
            "--entry-limit" => {
                index += 1;
                let value = argv.get(index).ok_or("--entry-limit requires a value")?;
                args.entry_limit = parse_limit(value)?;
            }
            "--all" => {
                args.entry_limit = usize::MAX;
            }
            "--compact" => {
                args.pretty = false;
            }
            "--pretty" => {
                args.pretty = true;
            }
            "-h" | "--help" => return Err(usage().to_string()),
            other => return Err(format!("unknown argument: {other}\n{}", usage())),
        }
        index += 1;
    }
    if args.input_path.is_none() {
        return Err(format!("--input is required\n{}", usage()));
    }
    Ok(args)
}

fn parse_object_type(value: &str) -> Result<ObjectType, String> {
    match value.to_ascii_lowercase().as_str() {
        "auto" => Ok(ObjectType::Auto),
        "cer" | ".cer" | "cert" | "certificate" => Ok(ObjectType::Cer),
        "mft" | ".mft" | "manifest" => Ok(ObjectType::Mft),
        "crl" | ".crl" => Ok(ObjectType::Crl),
        "roa" | ".roa" => Ok(ObjectType::Roa),
        "asa" | ".asa" | "aspa" => Ok(ObjectType::Aspa),
        _ => Err(format!("unsupported --type: {value}\n{}", usage())),
    }
}

fn parse_limit(value: &str) -> Result<usize, String> {
    if value.eq_ignore_ascii_case("all") {
        return Ok(usize::MAX);
    }
    value
        .parse::<usize>()
        .map_err(|_| format!("invalid --entry-limit: {value}"))
}

fn resolve_object_type(object_type: ObjectType, path: &Path) -> Result<ObjectType, String> {
    if object_type != ObjectType::Auto {
        return Ok(object_type);
    }
    match path
        .extension()
        .and_then(|v| v.to_str())
        .map(|v| v.to_ascii_lowercase())
        .as_deref()
    {
        Some("cer") => Ok(ObjectType::Cer),
        Some("mft") => Ok(ObjectType::Mft),
        Some("crl") => Ok(ObjectType::Crl),
        Some("roa") => Ok(ObjectType::Roa),
        Some("asa") | Some("aspa") => Ok(ObjectType::Aspa),
        _ => Err(format!(
            "cannot infer object type from path: {}",
            path.display()
        )),
    }
}

fn parse_object_json(
    object_type: ObjectType,
    input_path: &Path,
    bytes: &[u8],
    entry_limit: usize,
) -> Value {
    let object = match object_type {
        ObjectType::Auto => unreachable!("auto must be resolved"),
        ObjectType::Cer => parse_cer_json(bytes),
        ObjectType::Mft => parse_mft_json(bytes, entry_limit),
        ObjectType::Crl => parse_crl_json(bytes, entry_limit),
        ObjectType::Roa => parse_roa_json(bytes, entry_limit),
        ObjectType::Aspa => parse_aspa_json(bytes, entry_limit),
    };
    json!({
        "tool": "rpki_object_parse",
        "schemaVersion": 1,
        "input": {
            "path": input_path.display().to_string(),
            "type": object_type_label(object_type),
            "bytes": bytes_summary(bytes),
        },
        "object": object,
    })
}

fn parse_cer_json(bytes: &[u8]) -> Value {
    match ResourceCertificate::decode_der(bytes) {
        Ok(cert) => {
            let ta_profile = match TaCertificate::decode_der(bytes) {
                Ok(ta) => json!({
                    "valid": true,
                    "selfSignature": result_json(ta.verify_self_signature().map_err(|e| e.to_string())),
                }),
                Err(err) => json!({
                    "valid": false,
                    "error": err.to_string(),
                }),
            };
            json!({
                "type": "cer",
                "decode": {"profileValid": true},
                "resourceCertificate": resource_certificate_json(&cert),
                "trustAnchorProfile": ta_profile,
            })
        }
        Err(err) => json!({
            "type": "cer",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

fn parse_mft_json(bytes: &[u8], entry_limit: usize) -> Value {
    match ManifestObject::decode_der(bytes) {
        Ok(mft) => {
            let files = mft.manifest.parse_files();
            let (file_sample, file_list_error) = match files {
                Ok(entries) => (
                    json!({
                        "count": entries.len(),
                        "truncated": entries.len() > entry_limit,
                        "entries": entries.iter().take(entry_limit).map(|item| {
                            json!({"fileName": item.file_name, "hashHex": hex::encode(item.hash_bytes)})
                        }).collect::<Vec<_>>(),
                    }),
                    Value::Null,
                ),
                Err(err) => (Value::Null, json!(err.to_string())),
            };
            json!({
                "type": "mft",
                "decode": {"profileValid": true},
                "eContentType": mft.econtent_type,
                "signedObject": signed_object_json(&mft.signed_object),
                "manifest": {
                    "version": mft.manifest.version,
                    "manifestNumberHex": mft.manifest.manifest_number.to_hex_upper(),
                    "thisUpdate": format_time(mft.manifest.this_update),
                    "nextUpdate": format_time(mft.manifest.next_update),
                    "fileHashAlg": mft.manifest.file_hash_alg,
                    "fileCount": mft.manifest.file_count(),
                    "fileList": file_sample,
                    "fileListError": file_list_error,
                },
                "embeddedEeProfile": result_json(mft.validate_embedded_ee_cert().map_err(|e| e.to_string())),
                "cmsSignature": result_json(mft.signed_object.verify_signature().map_err(|e| e.to_string())),
            })
        }
        Err(err) => json!({
            "type": "mft",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

fn parse_crl_json(bytes: &[u8], entry_limit: usize) -> Value {
    match RpkixCrl::decode_der(bytes) {
        Ok(crl) => json!({
            "type": "crl",
            "decode": {"profileValid": true},
            "rawDer": bytes_summary(&crl.raw_der),
            "version": crl.version,
            "issuer": crl.issuer_dn,
            "signatureAlgorithm": crl.signature_algorithm_oid,
            "thisUpdate": format_time(crl.this_update.utc),
            "nextUpdate": format_time(crl.next_update.utc),
            "extensions": {
                "authorityKeyIdentifier": hex::encode(&crl.extensions.authority_key_identifier),
                "crlNumberHex": crl.extensions.crl_number.to_hex_upper(),
                "crlNumber": crl.extensions.crl_number.to_u64(),
            },
            "revokedCertificates": {
                "count": crl.revoked_certs.len(),
                "truncated": crl.revoked_certs.len() > entry_limit,
                "entries": crl.revoked_certs.iter().take(entry_limit).map(|item| {
                    json!({
                        "serialNumberHex": item.serial_number.to_hex_upper(),
                        "serialNumber": item.serial_number.to_u64(),
                        "revocationDate": format_time(item.revocation_date.utc),
                    })
                }).collect::<Vec<_>>(),
            },
        }),
        Err(err) => json!({
            "type": "crl",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

fn parse_roa_json(bytes: &[u8], entry_limit: usize) -> Value {
    match RoaObject::decode_der(bytes) {
        Ok(roa) => json!({
            "type": "roa",
            "decode": {"profileValid": true},
            "eContentType": roa.econtent_type,
            "signedObject": signed_object_json(&roa.signed_object),
            "roa": {
                "version": roa.roa.version,
                "asId": roa.roa.as_id,
                "ipAddressFamilies": roa.roa.ip_addr_blocks.iter().map(|family| {
                    json!({
                        "afi": format!("{:?}", family.afi),
                        "addressCount": family.addresses.len(),
                        "truncated": family.addresses.len() > entry_limit,
                        "addresses": family.addresses.iter().take(entry_limit).map(|entry| {
                            json!({
                                "prefix": roa_prefix_string(&entry.prefix),
                                "maxLength": entry.max_length,
                            })
                        }).collect::<Vec<_>>(),
                    })
                }).collect::<Vec<_>>(),
            },
            "embeddedEeProfile": result_json(roa.validate_embedded_ee_cert().map_err(|e| e.to_string())),
            "cmsSignature": result_json(roa.signed_object.verify_signature().map_err(|e| e.to_string())),
        }),
        Err(err) => json!({
            "type": "roa",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

fn parse_aspa_json(bytes: &[u8], entry_limit: usize) -> Value {
    match AspaObject::decode_der(bytes) {
        Ok(aspa) => json!({
            "type": "aspa",
            "decode": {"profileValid": true},
            "eContentType": aspa.econtent_type,
            "signedObject": signed_object_json(&aspa.signed_object),
            "aspa": {
                "version": aspa.aspa.version,
                "customerAsId": aspa.aspa.customer_as_id,
                "providerCount": aspa.aspa.provider_as_ids.len(),
                "providersTruncated": aspa.aspa.provider_as_ids.len() > entry_limit,
                "providerAsIds": aspa.aspa.provider_as_ids.iter().take(entry_limit).copied().collect::<Vec<_>>(),
            },
            "embeddedEeProfile": result_json(aspa.validate_embedded_ee_cert().map_err(|e| e.to_string())),
            "cmsSignature": result_json(aspa.signed_object.verify_signature().map_err(|e| e.to_string())),
        }),
        Err(err) => json!({
            "type": "aspa",
            "decode": {"profileValid": false, "error": err.to_string()},
        }),
    }
}

fn resource_certificate_json(cert: &ResourceCertificate) -> Value {
    let tbs = &cert.tbs;
    json!({
        "rawDer": bytes_summary(&cert.raw_der),
        "kind": format!("{:?}", cert.kind),
        "version": tbs.version,
        "serialNumberHex": hex::encode(tbs.serial_number.to_bytes_be()),
        "signatureAlgorithm": tbs.signature_algorithm,
        "issuer": tbs.issuer_name.to_string(),
        "subject": tbs.subject_name.to_string(),
        "validity": {
            "notBefore": format_time(tbs.validity_not_before),
            "notAfter": format_time(tbs.validity_not_after),
        },
        "subjectPublicKeyInfo": bytes_summary(&tbs.subject_public_key_info),
        "extensions": rc_extensions_json(&tbs.extensions),
    })
}

fn rc_extensions_json(ext: &RcExtensions) -> Value {
    json!({
        "basicConstraintsCa": ext.basic_constraints_ca,
        "subjectKeyIdentifier": ext.subject_key_identifier.as_ref().map(|v| hex::encode(v)),
        "authorityKeyIdentifier": ext.authority_key_identifier.as_ref().map(|v| hex::encode(v)),
        "crlDistributionPointsUris": ext.crl_distribution_points_uris,
        "caIssuersUris": ext.ca_issuers_uris,
        "subjectInfoAccess": subject_info_access_json(ext.subject_info_access.as_ref()),
        "certificatePoliciesOid": ext.certificate_policies_oid,
        "ipResources": serde_json::to_value(&ext.ip_resources).unwrap_or(Value::Null),
        "asResources": serde_json::to_value(&ext.as_resources).unwrap_or(Value::Null),
    })
}

fn subject_info_access_json(value: Option<&SubjectInfoAccess>) -> Value {
    match value {
        None => Value::Null,
        Some(SubjectInfoAccess::Ca(ca)) => json!({
            "kind": "ca",
            "accessDescriptions": ca.access_descriptions.iter().map(access_description_json).collect::<Vec<_>>(),
        }),
        Some(SubjectInfoAccess::Ee(ee)) => json!({
            "kind": "ee",
            "signedObjectUris": ee.signed_object_uris,
            "accessDescriptions": ee.access_descriptions.iter().map(access_description_json).collect::<Vec<_>>(),
        }),
    }
}

fn access_description_json(value: &AccessDescription) -> Value {
    json!({
        "accessMethodOid": value.access_method_oid,
        "accessLocation": value.access_location,
    })
}

fn signed_object_json(signed_object: &RpkiSignedObject) -> Value {
    let signed_data = &signed_object.signed_data;
    json!({
        "rawDer": bytes_summary(&signed_object.raw_der),
        "contentInfoContentType": signed_object.content_info_content_type,
        "signedData": {
            "version": signed_data.version,
            "digestAlgorithms": signed_data.digest_algorithms,
            "encapContentInfo": {
                "eContentType": signed_data.encap_content_info.econtent_type,
                "eContent": bytes_summary(&signed_data.encap_content_info.econtent),
            },
            "certificates": signed_data.certificates.iter().map(ee_certificate_json).collect::<Vec<_>>(),
            "crlsPresent": signed_data.crls_present,
            "signerInfos": signed_data.signer_infos.iter().map(signer_info_json).collect::<Vec<_>>(),
        },
    })
}

fn ee_certificate_json(cert: &ResourceEeCertificate) -> Value {
    json!({
        "rawDer": bytes_summary(&cert.raw_der),
        "subjectKeyIdentifier": hex::encode(&cert.subject_key_identifier),
        "spkiDer": bytes_summary(&cert.spki_der),
        "rsaPublicKey": {
            "modulus": bytes_summary(&cert.rsa_public_modulus),
            "exponent": bytes_summary(&cert.rsa_public_exponent),
        },
        "tbsCertificate": bytes_summary(&cert.tbs_certificate_der),
        "certificateSignature": bytes_summary(&cert.signature_bytes),
        "keyUsageSummary": format!("{:?}", cert.key_usage_summary),
        "siaSignedObjectUris": cert.sia_signed_object_uris,
        "resourceCertificate": resource_certificate_json(&cert.resource_cert),
    })
}

fn signer_info_json(info: &SignerInfoProfiled) -> Value {
    json!({
        "version": info.version,
        "sidSki": hex::encode(&info.sid_ski),
        "digestAlgorithm": info.digest_algorithm,
        "signatureAlgorithm": info.signature_algorithm,
        "signedAttrs": signed_attrs_json(&info.signed_attrs),
        "unsignedAttrsPresent": info.unsigned_attrs_present,
        "signature": bytes_summary(&info.signature),
        "signedAttrsDerForSignature": bytes_summary(&info.signed_attrs_der_for_signature),
    })
}

fn signed_attrs_json(attrs: &SignedAttrsProfiled) -> Value {
    json!({
        "contentType": attrs.content_type,
        "messageDigest": hex::encode(&attrs.message_digest),
        "signingTime": {
            "utc": format_time(attrs.signing_time.utc),
            "encoding": format!("{:?}", attrs.signing_time.encoding),
        },
        "otherAttrsPresent": attrs.other_attrs_present,
    })
}

fn result_json(result: Result<(), String>) -> Value {
    match result {
        Ok(()) => json!({"valid": true}),
        Err(err) => json!({"valid": false, "error": err}),
    }
}

fn object_type_label(object_type: ObjectType) -> &'static str {
    match object_type {
        ObjectType::Auto => "auto",
        ObjectType::Cer => "cer",
        ObjectType::Mft => "mft",
        ObjectType::Crl => "crl",
        ObjectType::Roa => "roa",
        ObjectType::Aspa => "aspa",
    }
}

fn bytes_summary(bytes: &[u8]) -> Value {
    let head_len = bytes.len().min(16);
    let tail_len = bytes.len().min(16);
    json!({
        "len": bytes.len(),
        "sha256": sha256_hex(bytes),
        "headHex": hex::encode(&bytes[..head_len]),
        "tailHex": hex::encode(&bytes[bytes.len().saturating_sub(tail_len)..]),
    })
}

fn sha256_hex(bytes: &[u8]) -> String {
    hex::encode(Sha256::digest(bytes))
}

fn format_time(value: time::OffsetDateTime) -> String {
    value
        .to_offset(time::UtcOffset::UTC)
        .format(&time::format_description::well_known::Rfc3339)
        .unwrap_or_else(|_| value.unix_timestamp().to_string())
}

fn roa_prefix_string(prefix: &RoaIpPrefix) -> String {
    let bytes = prefix.addr_bytes();
    match prefix.afi {
        RoaAfi::Ipv4 => {
            let octets = [bytes[0], bytes[1], bytes[2], bytes[3]];
            format!("{}/{}", Ipv4Addr::from(octets), prefix.prefix_len)
        }
        RoaAfi::Ipv6 => {
            let mut octets = [0u8; 16];
            octets.copy_from_slice(bytes);
            format!("{}/{}", Ipv6Addr::from(octets), prefix.prefix_len)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_args_accepts_plan_shape() {
        let args = parse_args(&[
            "rpki_object_parse".to_string(),
            "--type".to_string(),
            "auto".to_string(),
            "--input".to_string(),
            "a.roa".to_string(),
            "--out".to_string(),
            "parsed.json".to_string(),
            "--entry-limit".to_string(),
            "5".to_string(),
        ])
        .expect("parse args");
        assert_eq!(args.object_type, ObjectType::Auto);
        assert_eq!(args.input_path.as_deref(), Some(Path::new("a.roa")));
        assert_eq!(args.out_path.as_deref(), Some(Path::new("parsed.json")));
        assert_eq!(args.entry_limit, 5);
    }

    #[test]
    fn parse_args_accepts_aspa_alias_and_all_limit() {
        let args = parse_args(&[
            "rpki_object_parse".to_string(),
            "--type".to_string(),
            "asa".to_string(),
            "--in".to_string(),
            "a.asa".to_string(),
            "--entry-limit".to_string(),
            "all".to_string(),
            "--compact".to_string(),
        ])
        .expect("parse args");
        assert_eq!(args.object_type, ObjectType::Aspa);
        assert_eq!(args.entry_limit, usize::MAX);
        assert!(!args.pretty);
    }

    #[test]
    fn resolve_auto_type_from_extension() {
        assert_eq!(
            resolve_object_type(ObjectType::Auto, Path::new("a.mft")).expect("resolve"),
            ObjectType::Mft
        );
        assert_eq!(
            resolve_object_type(ObjectType::Auto, Path::new("a.asa")).expect("resolve"),
            ObjectType::Aspa
        );
    }

    #[test]
    fn unknown_auto_type_is_rejected() {
        let err = resolve_object_type(ObjectType::Auto, Path::new("a.bin")).unwrap_err();
        assert!(err.contains("cannot infer object type"), "{err}");
    }
}
