use crate::data_model::common::DerReader;
use crate::data_model::oid::{OID_CT_RPKI_CCR_RAW, OID_SHA256_RAW};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CcrStateDigestSummary {
    pub version: u32,
    pub hash_alg_oid: String,
    pub mfts: Option<Vec<u8>>,
    pub vrps: Option<Vec<u8>>,
    pub vaps: Option<Vec<u8>>,
    pub tas: Option<Vec<u8>>,
    pub rks: Option<Vec<u8>>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CcrStateDigestComparison {
    pub ours: CcrStateDigestSummary,
    pub peer: CcrStateDigestSummary,
    pub states: Vec<CcrStateDigestStateComparison>,
}

impl CcrStateDigestComparison {
    pub fn matches(&self) -> bool {
        self.ours.version == self.peer.version
            && self.ours.hash_alg_oid == self.peer.hash_alg_oid
            && self.states.iter().all(|state| state.matches)
    }

    pub fn mismatched_state_names(&self) -> Vec<&'static str> {
        self.states
            .iter()
            .filter(|state| !state.matches)
            .map(|state| state.name)
            .collect()
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CcrStateDigestStateComparison {
    pub name: &'static str,
    pub ours_present: bool,
    pub peer_present: bool,
    pub ours_hash_hex: Option<String>,
    pub peer_hash_hex: Option<String>,
    pub matches: bool,
}

#[derive(Debug, thiserror::Error)]
pub enum CcrStateDigestError {
    #[error("DER parse error: {0}")]
    Parse(String),

    #[error("unexpected contentType OID")]
    UnexpectedContentType,

    #[error("unexpected digest algorithm OID")]
    UnexpectedDigestAlgorithm,

    #[error("unexpected CCR field tag 0x{0:02X}")]
    UnexpectedCcrField(u8),
}

pub fn decode_state_digest_summary(
    der: &[u8],
) -> Result<CcrStateDigestSummary, CcrStateDigestError> {
    let mut top = DerReader::new(der);
    let mut seq = top.take_sequence().map_err(CcrStateDigestError::Parse)?;
    if !top.is_empty() {
        return Err(CcrStateDigestError::Parse(
            "trailing bytes after ContentInfo".into(),
        ));
    }
    let content_type_raw = seq.take_tag(0x06).map_err(CcrStateDigestError::Parse)?;
    if content_type_raw != OID_CT_RPKI_CCR_RAW {
        return Err(CcrStateDigestError::UnexpectedContentType);
    }
    let inner = seq.take_tag(0xA0).map_err(CcrStateDigestError::Parse)?;
    if !seq.is_empty() {
        return Err(CcrStateDigestError::Parse(
            "trailing fields in ContentInfo".into(),
        ));
    }
    decode_ccr_state_digest_summary(inner)
}

pub fn compare_state_digests(
    ours_der: &[u8],
    peer_der: &[u8],
) -> Result<CcrStateDigestComparison, CcrStateDigestError> {
    let ours = decode_state_digest_summary(ours_der)?;
    let peer = decode_state_digest_summary(peer_der)?;
    let states = vec![
        compare_state("mfts", &ours.mfts, &peer.mfts),
        compare_state("vrps", &ours.vrps, &peer.vrps),
        compare_state("vaps", &ours.vaps, &peer.vaps),
        compare_state("tas", &ours.tas, &peer.tas),
        compare_state("rks", &ours.rks, &peer.rks),
    ];
    Ok(CcrStateDigestComparison { ours, peer, states })
}

fn decode_ccr_state_digest_summary(
    der: &[u8],
) -> Result<CcrStateDigestSummary, CcrStateDigestError> {
    let mut top = DerReader::new(der);
    let mut seq = top.take_sequence().map_err(CcrStateDigestError::Parse)?;
    if !top.is_empty() {
        return Err(CcrStateDigestError::Parse(
            "trailing bytes after CCR".into(),
        ));
    }

    let version = if !seq.is_empty() && seq.peek_tag().map_err(CcrStateDigestError::Parse)? == 0xA0
    {
        let explicit = seq.take_tag(0xA0).map_err(CcrStateDigestError::Parse)?;
        let mut inner = DerReader::new(explicit);
        let version = inner.take_uint_u64().map_err(CcrStateDigestError::Parse)? as u32;
        if !inner.is_empty() {
            return Err(CcrStateDigestError::Parse(
                "trailing bytes inside CCR version EXPLICIT".into(),
            ));
        }
        version
    } else {
        crate::ccr::model::CCR_VERSION_V0
    };

    let hash_alg_oid =
        decode_digest_algorithm_oid(seq.take_sequence().map_err(CcrStateDigestError::Parse)?)?;
    let _produced_at = seq.take_tag(0x18).map_err(CcrStateDigestError::Parse)?;

    let mut mfts = None;
    let mut vrps = None;
    let mut vaps = None;
    let mut tas = None;
    let mut rks = None;
    while !seq.is_empty() {
        let (tag, value) = seq.take_any().map_err(CcrStateDigestError::Parse)?;
        match tag {
            0xA1 => mfts = Some(read_state_hash(value, StateShape::Manifest)?),
            0xA2 => vrps = Some(read_state_hash(value, StateShape::PayloadThenHash)?),
            0xA3 => vaps = Some(read_state_hash(value, StateShape::PayloadThenHash)?),
            0xA4 => tas = Some(read_state_hash(value, StateShape::PayloadThenHash)?),
            0xA5 => rks = Some(read_state_hash(value, StateShape::PayloadThenHash)?),
            other => return Err(CcrStateDigestError::UnexpectedCcrField(other)),
        }
    }

    Ok(CcrStateDigestSummary {
        version,
        hash_alg_oid,
        mfts,
        vrps,
        vaps,
        tas,
        rks,
    })
}

#[derive(Clone, Copy)]
enum StateShape {
    Manifest,
    PayloadThenHash,
}

fn read_state_hash(explicit_der: &[u8], shape: StateShape) -> Result<Vec<u8>, CcrStateDigestError> {
    let mut outer = DerReader::new(explicit_der);
    let mut seq = outer.take_sequence().map_err(CcrStateDigestError::Parse)?;
    if !outer.is_empty() {
        return Err(CcrStateDigestError::Parse(
            "trailing bytes after CCR state".into(),
        ));
    }
    match shape {
        StateShape::Manifest => {
            seq.skip_any().map_err(CcrStateDigestError::Parse)?;
            seq.take_tag(0x18).map_err(CcrStateDigestError::Parse)?;
        }
        StateShape::PayloadThenHash => {
            seq.skip_any().map_err(CcrStateDigestError::Parse)?;
        }
    }
    let hash = seq
        .take_octet_string()
        .map_err(CcrStateDigestError::Parse)?
        .to_vec();
    if hash.len() != crate::ccr::model::DIGEST_LEN_SHA256 {
        return Err(CcrStateDigestError::Parse(format!(
            "state hash must be {} bytes, got {}",
            crate::ccr::model::DIGEST_LEN_SHA256,
            hash.len()
        )));
    }
    if !seq.is_empty() {
        return Err(CcrStateDigestError::Parse(
            "trailing fields after CCR state hash".into(),
        ));
    }
    Ok(hash)
}

fn decode_digest_algorithm_oid(mut seq: DerReader<'_>) -> Result<String, CcrStateDigestError> {
    let oid_raw = seq.take_tag(0x06).map_err(CcrStateDigestError::Parse)?;
    if oid_raw != OID_SHA256_RAW {
        return Err(CcrStateDigestError::UnexpectedDigestAlgorithm);
    }
    if !seq.is_empty() {
        let tag = seq.peek_tag().map_err(CcrStateDigestError::Parse)?;
        if tag == 0x05 {
            let null = seq.take_tag(0x05).map_err(CcrStateDigestError::Parse)?;
            if !null.is_empty() {
                return Err(CcrStateDigestError::Parse(
                    "AlgorithmIdentifier NULL parameters must be empty".into(),
                ));
            }
        }
    }
    if !seq.is_empty() {
        return Err(CcrStateDigestError::Parse(
            "trailing fields in DigestAlgorithmIdentifier".into(),
        ));
    }
    Ok(crate::data_model::oid::OID_SHA256.to_string())
}

fn compare_state(
    name: &'static str,
    ours: &Option<Vec<u8>>,
    peer: &Option<Vec<u8>>,
) -> CcrStateDigestStateComparison {
    CcrStateDigestStateComparison {
        name,
        ours_present: ours.is_some(),
        peer_present: peer.is_some(),
        ours_hash_hex: ours.as_ref().map(hex::encode),
        peer_hash_hex: peer.as_ref().map(hex::encode),
        matches: ours == peer,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ccr::{
        CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation,
        build_aspa_payload_state, build_roa_payload_state, encode_content_info,
    };
    use crate::data_model::roa::{IpPrefix, RoaAfi};
    use crate::validation::objects::{AspaAttestation, Vrp};

    fn sample_content(produced_at: time::OffsetDateTime) -> CcrContentInfo {
        let vrps = build_roa_payload_state(&[Vrp {
            asn: 64496,
            prefix: IpPrefix {
                afi: RoaAfi::Ipv4,
                prefix_len: 24,
                addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            },
            max_length: 24,
        }])
        .expect("build vrps");
        let vaps = build_aspa_payload_state(&[AspaAttestation {
            customer_as_id: 64496,
            provider_as_ids: vec![64497],
        }])
        .expect("build vaps");
        CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
            version: 0,
            hash_alg: CcrDigestAlgorithm::Sha256,
            produced_at,
            mfts: None,
            vrps: Some(vrps),
            vaps: Some(vaps),
            tas: None,
            rks: None,
        })
    }

    #[test]
    fn decode_state_digest_summary_extracts_hashes_without_full_model() {
        let content = sample_content(time::OffsetDateTime::UNIX_EPOCH);
        let der = encode_content_info(&content).expect("encode");
        let summary = decode_state_digest_summary(&der).expect("summary");
        assert_eq!(summary.version, 0);
        assert_eq!(summary.hash_alg_oid, crate::data_model::oid::OID_SHA256);
        assert_eq!(
            summary.vrps.as_deref(),
            content
                .content
                .vrps
                .as_ref()
                .map(|state| state.hash.as_slice())
        );
        assert_eq!(
            summary.vaps.as_deref(),
            content
                .content
                .vaps
                .as_ref()
                .map(|state| state.hash.as_slice())
        );
        assert!(summary.mfts.is_none());
        assert!(summary.tas.is_none());
        assert!(summary.rks.is_none());
    }

    #[test]
    fn compare_state_digests_ignores_produced_at() {
        let first = encode_content_info(&sample_content(time::OffsetDateTime::UNIX_EPOCH))
            .expect("encode first");
        let second = encode_content_info(&sample_content(
            time::OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(60),
        ))
        .expect("encode second");
        let comparison = compare_state_digests(&first, &second).expect("compare");
        assert!(comparison.matches());
        assert!(comparison.mismatched_state_names().is_empty());
    }

    #[test]
    fn compare_state_digests_reports_mismatched_state() {
        let first = encode_content_info(&sample_content(time::OffsetDateTime::UNIX_EPOCH))
            .expect("encode first");
        let mut second_content =
            sample_content(time::OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(60));
        second_content.content.vaps.as_mut().expect("vaps").hash[0] ^= 0xFF;
        let second = encode_content_info(&second_content).expect("encode second");
        let comparison = compare_state_digests(&first, &second).expect("compare");
        assert!(!comparison.matches());
        assert_eq!(comparison.mismatched_state_names(), vec!["vaps"]);
    }
}
