use crate::data_model::common::DerReader;
use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
use crate::data_model::rc::AccessDescription;

pub(crate) fn select_manifest_signed_object_location(
    manifest_rsync_uri: &str,
    access_descriptions: &[AccessDescription],
) -> Result<Vec<u8>, String> {
    let matching = access_descriptions
        .iter()
        .filter(|access_description| {
            access_description.access_method_oid == OID_AD_SIGNED_OBJECT
                && access_description.access_location == manifest_rsync_uri
        })
        .collect::<Vec<_>>();
    let access_description = expect_single_matching_location(
        manifest_rsync_uri,
        matching.len(),
        "parsed Manifest EE SIA",
    )?;
    encode_access_description_der(matching[access_description])
}

pub(crate) fn select_manifest_signed_object_location_from_der(
    manifest_rsync_uri: &str,
    locations_der: &[Vec<u8>],
) -> Result<Vec<u8>, String> {
    let matching = locations_der
        .iter()
        .filter_map(
            |location_der| match decode_access_description_der(location_der) {
                Ok(access_description)
                    if access_description.access_method_oid == OID_AD_SIGNED_OBJECT
                        && access_description.access_location == manifest_rsync_uri =>
                {
                    Some(Ok(location_der.clone()))
                }
                Ok(_) => None,
                Err(detail) => Some(Err(detail)),
            },
        )
        .collect::<Result<Vec<_>, _>>()?;
    let index = expect_single_matching_location(
        manifest_rsync_uri,
        matching.len(),
        "persisted VCIR CCR projection",
    )?;
    Ok(matching[index].clone())
}

pub(crate) fn encode_access_description_der(
    access_description: &AccessDescription,
) -> Result<Vec<u8>, String> {
    let oid = encode_oid_der(&access_description.access_method_oid)?;
    let uri = encode_tlv(0x86, access_description.access_location.as_bytes().to_vec());
    Ok(encode_sequence(&[oid, uri]))
}

fn expect_single_matching_location(
    manifest_rsync_uri: &str,
    matching_count: usize,
    source: &str,
) -> Result<usize, String> {
    if matching_count == 1 {
        return Ok(0);
    }
    Err(format!(
        "{source} contains {matching_count} id-ad-signedObject locations matching manifest URI {manifest_rsync_uri}; expected exactly one"
    ))
}

fn decode_access_description_der(der: &[u8]) -> Result<AccessDescription, String> {
    let mut top = DerReader::new(der);
    let mut sequence = top.take_sequence()?;
    if !top.is_empty() {
        return Err("trailing bytes after AccessDescription".to_string());
    }
    let access_method_oid = decode_oid_der(sequence.take_tag(0x06)?)?;
    let access_location = std::str::from_utf8(sequence.take_tag(0x86)?)
        .map_err(|error| format!("AccessDescription URI is not UTF-8: {error}"))?
        .to_string();
    if !sequence.is_empty() {
        return Err("trailing fields in AccessDescription".to_string());
    }
    Ok(AccessDescription {
        access_method_oid,
        access_location,
    })
}

fn decode_oid_der(value: &[u8]) -> Result<String, String> {
    let mut offset = 0usize;
    let first = decode_base128(value, &mut offset)?;
    let (first_arc, second_arc) = match first {
        0..=39 => (0, first),
        40..=79 => (1, first - 40),
        value => (2, value - 80),
    };
    let mut arcs = vec![first_arc, second_arc];
    while offset < value.len() {
        arcs.push(decode_base128(value, &mut offset)?);
    }
    Ok(arcs
        .into_iter()
        .map(|arc| arc.to_string())
        .collect::<Vec<_>>()
        .join("."))
}

fn decode_base128(value: &[u8], offset: &mut usize) -> Result<u64, String> {
    let first = *value
        .get(*offset)
        .ok_or_else(|| "truncated OBJECT IDENTIFIER".to_string())?;
    if first == 0x80 {
        return Err("non-minimal OBJECT IDENTIFIER base-128 encoding".to_string());
    }
    let mut out = 0u64;
    loop {
        let byte = *value
            .get(*offset)
            .ok_or_else(|| "truncated OBJECT IDENTIFIER".to_string())?;
        *offset += 1;
        out = out
            .checked_shl(7)
            .ok_or_else(|| "OBJECT IDENTIFIER arc overflows u64".to_string())?
            .checked_add((byte & 0x7f) as u64)
            .ok_or_else(|| "OBJECT IDENTIFIER arc overflows u64".to_string())?;
        if byte & 0x80 == 0 {
            return Ok(out);
        }
    }
}

fn encode_oid_der(oid: &str) -> Result<Vec<u8>, String> {
    let arcs = oid
        .split('.')
        .map(|part| {
            part.parse::<u64>()
                .map_err(|_| format!("unsupported accessMethod OID: {oid}"))
        })
        .collect::<Result<Vec<_>, _>>()?;
    if arcs.len() < 2 || arcs[0] > 2 || (arcs[0] < 2 && arcs[1] >= 40) {
        return Err(format!("unsupported accessMethod OID: {oid}"));
    }
    let mut body = Vec::new();
    encode_base128(
        arcs[0]
            .checked_mul(40)
            .and_then(|value| value.checked_add(arcs[1]))
            .ok_or_else(|| format!("unsupported accessMethod OID: {oid}"))?,
        &mut body,
    );
    for arc in &arcs[2..] {
        encode_base128(*arc, &mut body);
    }
    Ok(encode_tlv(0x06, body))
}

fn encode_base128(mut value: u64, out: &mut Vec<u8>) {
    let mut encoded = vec![(value & 0x7f) as u8];
    value >>= 7;
    while value > 0 {
        encoded.push(((value & 0x7f) as u8) | 0x80);
        value >>= 7;
    }
    encoded.reverse();
    out.extend_from_slice(&encoded);
}

fn encode_sequence(elements: &[Vec<u8>]) -> Vec<u8> {
    let total_len = elements.iter().map(Vec::len).sum();
    let mut value = Vec::with_capacity(total_len);
    for element in elements {
        value.extend_from_slice(element);
    }
    encode_tlv(0x30, value)
}

fn encode_tlv(tag: u8, value: Vec<u8>) -> Vec<u8> {
    let mut out = Vec::with_capacity(1 + 9 + value.len());
    out.push(tag);
    encode_length(value.len(), &mut out);
    out.extend_from_slice(&value);
    out
}

fn encode_length(len: usize, out: &mut Vec<u8>) {
    if len < 0x80 {
        out.push(len as u8);
        return;
    }
    let mut bytes = Vec::new();
    let mut value = len;
    while value > 0 {
        bytes.push((value & 0xff) as u8);
        value >>= 8;
    }
    bytes.reverse();
    out.push(0x80 | bytes.len() as u8);
    out.extend_from_slice(&bytes);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::data_model::oid::OID_AD_RPKI_NOTIFY;

    const MANIFEST_URI: &str = "rsync://example.test/repo/manifest.mft";

    fn access_description(access_method_oid: &str, access_location: &str) -> AccessDescription {
        AccessDescription {
            access_method_oid: access_method_oid.to_string(),
            access_location: access_location.to_string(),
        }
    }

    #[test]
    fn selects_matching_signed_object_and_excludes_rpki_notify() {
        let signed_object = access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
        let selected = select_manifest_signed_object_location(
            MANIFEST_URI,
            &[
                signed_object.clone(),
                access_description(
                    OID_AD_RPKI_NOTIFY,
                    "https://rrdp.example.test/notification.xml",
                ),
            ],
        )
        .expect("select signed object");

        assert_eq!(
            selected,
            encode_access_description_der(&signed_object).unwrap()
        );
    }

    #[test]
    fn selects_only_the_signed_object_matching_manifest_uri() {
        let expected = access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
        let selected = select_manifest_signed_object_location(
            MANIFEST_URI,
            &[
                access_description(
                    OID_AD_SIGNED_OBJECT,
                    "https://backup.example.test/manifest.mft",
                ),
                expected.clone(),
            ],
        )
        .expect("select matching signed object");

        assert_eq!(selected, encode_access_description_der(&expected).unwrap());
    }

    #[test]
    fn rejects_missing_or_duplicate_matching_signed_object() {
        let missing = select_manifest_signed_object_location(
            MANIFEST_URI,
            &[access_description(
                OID_AD_RPKI_NOTIFY,
                "https://rrdp.example.test/notification.xml",
            )],
        )
        .expect_err("missing signed object must fail");
        assert!(missing.contains("contains 0"), "{missing}");

        let duplicate = select_manifest_signed_object_location(
            MANIFEST_URI,
            &[
                access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI),
                access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI),
            ],
        )
        .expect_err("duplicate signed object must fail");
        assert!(duplicate.contains("contains 2"), "{duplicate}");
    }

    #[test]
    fn selects_matching_signed_object_from_historical_projection() {
        let signed_object = access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
        let signed_object_der = encode_access_description_der(&signed_object).unwrap();
        let notify_der = encode_access_description_der(&access_description(
            OID_AD_RPKI_NOTIFY,
            "https://rrdp.example.test/notification.xml",
        ))
        .unwrap();

        let selected = select_manifest_signed_object_location_from_der(
            MANIFEST_URI,
            &[notify_der, signed_object_der.clone()],
        )
        .expect("select historical signed object");
        assert_eq!(selected, signed_object_der);
    }

    #[test]
    fn rejects_malformed_historical_projection() {
        let error = select_manifest_signed_object_location_from_der(
            MANIFEST_URI,
            &[vec![0x30, 0x01, 0x06]],
        )
        .expect_err("malformed historical projection must fail");
        assert!(error.contains("truncated DER"), "{error}");
    }

    #[test]
    fn access_description_der_codec_covers_long_and_invalid_forms() {
        let long = access_description(
            "2.999.200.1",
            &format!("rsync://example.test/repo/{}", "x".repeat(160)),
        );
        let encoded = encode_access_description_der(&long).expect("encode long access description");
        assert_eq!(
            decode_access_description_der(&encoded).expect("decode long access description"),
            long
        );

        let mut trailing = encoded.clone();
        trailing.push(0);
        assert!(
            decode_access_description_der(&trailing)
                .expect_err("trailing bytes must fail")
                .contains("trailing bytes")
        );

        let oid = encode_oid_der(OID_AD_SIGNED_OBJECT).expect("encode signedObject OID");
        let uri = encode_tlv(0x86, MANIFEST_URI.as_bytes().to_vec());
        assert!(
            decode_access_description_der(&encode_sequence(&[
                oid.clone(),
                uri.clone(),
                encode_tlv(0x05, Vec::new()),
            ]))
            .expect_err("trailing field must fail")
            .contains("trailing fields")
        );
        assert!(
            decode_access_description_der(&encode_sequence(&[oid, encode_tlv(0x86, vec![0xff]),]))
                .expect_err("non-UTF8 URI must fail")
                .contains("not UTF-8")
        );
        assert!(
            decode_access_description_der(&encode_sequence(&[
                encode_tlv(0x06, vec![0x80, 0x00]),
                uri.clone(),
            ]))
            .expect_err("non-minimal OID must fail")
            .contains("non-minimal")
        );
        assert!(
            decode_access_description_der(&encode_sequence(&[encode_tlv(0x06, vec![0x81]), uri,]))
                .expect_err("truncated OID must fail")
                .contains("truncated OBJECT IDENTIFIER")
        );
        assert!(
            encode_access_description_der(&access_description("3.1", MANIFEST_URI))
                .expect_err("invalid OID must fail")
                .contains("unsupported accessMethod OID")
        );
    }

    #[test]
    fn access_description_der_codec_reports_malformed_selector_inputs() {
        let malformed_sequence = decode_access_description_der(&[0x31, 0x00])
            .expect_err("non-sequence AccessDescription must fail");
        assert!(!malformed_sequence.is_empty());

        let missing_method = decode_access_description_der(&encode_sequence(&[encode_tlv(
            0x86,
            MANIFEST_URI.as_bytes().to_vec(),
        )]))
        .expect_err("missing accessMethod must fail");
        assert!(!missing_method.is_empty());

        let oid = encode_oid_der(OID_AD_SIGNED_OBJECT).expect("encode signedObject OID");
        let missing_location = decode_access_description_der(&encode_sequence(&[oid.clone()]))
            .expect_err("missing accessLocation must fail");
        assert!(!missing_location.is_empty());

        let wrong_location_tag = decode_access_description_der(&encode_sequence(&[
            oid.clone(),
            encode_tlv(0x04, MANIFEST_URI.as_bytes().to_vec()),
        ]))
        .expect_err("wrong accessLocation tag must fail");
        assert!(!wrong_location_tag.is_empty());

        let empty_oid = decode_access_description_der(&encode_sequence(&[
            encode_tlv(0x06, Vec::new()),
            encode_tlv(0x86, MANIFEST_URI.as_bytes().to_vec()),
        ]))
        .expect_err("empty OID must fail");
        assert!(!empty_oid.is_empty());

        let first_arc_zero = decode_access_description_der(&encode_sequence(&[
            encode_tlv(0x06, vec![0x01, 0x02]),
            encode_tlv(0x86, MANIFEST_URI.as_bytes().to_vec()),
        ]))
        .expect("decode first OID arc zero");
        assert_eq!(first_arc_zero.access_method_oid, "0.1.2");
        assert_eq!(first_arc_zero.access_location, MANIFEST_URI);

        let non_numeric =
            encode_access_description_der(&access_description("no.such.oid", MANIFEST_URI))
                .expect_err("non-numeric OID must fail");
        assert!(!non_numeric.is_empty());

        let too_short = encode_access_description_der(&access_description("1", MANIFEST_URI))
            .expect_err("OID without second arc must fail");
        assert!(!too_short.is_empty());

        let invalid_second_arc =
            encode_access_description_der(&access_description("1.40", MANIFEST_URI))
                .expect_err("invalid second OID arc must fail");
        assert!(!invalid_second_arc.is_empty());

        let overflowing_first_subidentifier = encode_access_description_der(&access_description(
            "2.18446744073709551615",
            MANIFEST_URI,
        ))
        .expect_err("overflowing first OID subidentifier must fail");
        assert!(!overflowing_first_subidentifier.is_empty());
    }
}
