use crate::data_model::oid::OID_SHA256;

pub const CIR_VERSION_V1: u32 = 1;
pub const CIR_VERSION_V2: u32 = 2;
pub const CIR_VERSION_V3: u32 = 3;
pub const CIR_VERSION_V4: u32 = 4;
pub const DIGEST_LEN_SHA256: usize = 32;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CirHashAlgorithm {
    Sha256,
}

impl CirHashAlgorithm {
    pub fn oid(&self) -> &'static str {
        match self {
            Self::Sha256 => OID_SHA256,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanonicalInputRepresentation {
    pub version: u32,
    pub hash_alg: CirHashAlgorithm,
    pub validation_time: time::OffsetDateTime,
    pub fresh_validated_objects: Vec<CirObject>,
    pub cached_validated_objects: Vec<CirObject>,
    pub trust_anchors: Vec<CirTrustAnchor>,
    pub object_list_sha256: Vec<u8>,
    pub fresh_object_list_sha256: Vec<u8>,
    pub cached_object_list_sha256: Vec<u8>,
    pub reject_list_sha256: Vec<u8>,
    pub fresh_reject_list_sha256: Vec<u8>,
    pub cached_reject_list_sha256: Vec<u8>,
    pub fresh_rejected_objects: Vec<CirRejectedObject>,
    pub cached_rejected_objects: Vec<CirRejectedObject>,
}

impl CanonicalInputRepresentation {
    pub fn new_v4(
        validation_time: time::OffsetDateTime,
        fresh_validated_objects: Vec<CirObject>,
        cached_validated_objects: Vec<CirObject>,
        trust_anchors: Vec<CirTrustAnchor>,
        fresh_rejected_objects: Vec<CirRejectedObject>,
        cached_rejected_objects: Vec<CirRejectedObject>,
    ) -> Self {
        let fresh_object_list_sha256 = compute_object_list_sha256(fresh_validated_objects.iter());
        let cached_object_list_sha256 = compute_object_list_sha256(cached_validated_objects.iter());
        let object_list_sha256 = compute_sectioned_object_list_sha256(
            fresh_validated_objects
                .iter()
                .map(|object| ("fresh", object))
                .chain(
                    cached_validated_objects
                        .iter()
                        .map(|object| ("cached", object)),
                ),
        );
        let fresh_reject_list_sha256 = compute_reject_list_sha256(
            fresh_rejected_objects
                .iter()
                .map(|item| item.object_uri.as_str()),
        );
        let cached_reject_list_sha256 = compute_reject_list_sha256(
            cached_rejected_objects
                .iter()
                .map(|item| item.object_uri.as_str()),
        );
        let mut all_rejected_uris = fresh_rejected_objects
            .iter()
            .chain(cached_rejected_objects.iter())
            .map(|item| item.object_uri.as_str())
            .collect::<Vec<_>>();
        all_rejected_uris.sort_unstable();
        all_rejected_uris.dedup();
        let reject_list_sha256 = compute_reject_list_sha256(all_rejected_uris.iter().copied());

        Self {
            version: CIR_VERSION_V4,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: validation_time.to_offset(time::UtcOffset::UTC),
            fresh_validated_objects,
            cached_validated_objects,
            trust_anchors,
            object_list_sha256,
            fresh_object_list_sha256,
            cached_object_list_sha256,
            reject_list_sha256,
            fresh_reject_list_sha256,
            cached_reject_list_sha256,
            fresh_rejected_objects,
            cached_rejected_objects,
        }
    }

    pub fn validate(&self) -> Result<(), String> {
        if self.version != CIR_VERSION_V4 {
            return Err(format!(
                "CIR version must be {CIR_VERSION_V4}, got {}",
                self.version
            ));
        }
        if !matches!(self.hash_alg, CirHashAlgorithm::Sha256) {
            return Err("CIR hashAlg must be SHA-256".into());
        }
        if self.validation_time.offset() != time::UtcOffset::UTC {
            return Err("CIR validationTime must be UTC".into());
        }
        validate_sorted_unique_strings(
            self.fresh_validated_objects
                .iter()
                .map(|item| item.rsync_uri.as_str()),
            "CIR.freshValidatedObjects must be sorted by rsyncUri and unique",
        )?;
        validate_sorted_unique_strings(
            self.cached_validated_objects
                .iter()
                .map(|item| item.rsync_uri.as_str()),
            "CIR.cachedValidatedObjects must be sorted by rsyncUri and unique",
        )?;
        validate_sorted_unique_strings(
            self.trust_anchors
                .iter()
                .map(|item| item.ta_rsync_uri.as_str()),
            "CIR.trustAnchors must be sorted by taRsyncUri and unique",
        )?;
        validate_sorted_unique_strings(
            self.fresh_rejected_objects
                .iter()
                .map(|item| item.object_uri.as_str()),
            "CIR.freshRejectedObjects must be sorted by objectUri and unique",
        )?;
        validate_sorted_unique_strings(
            self.cached_rejected_objects
                .iter()
                .map(|item| item.object_uri.as_str()),
            "CIR.cachedRejectedObjects must be sorted by objectUri and unique",
        )?;
        let object_uris = self
            .fresh_validated_objects
            .iter()
            .chain(self.cached_validated_objects.iter())
            .map(|item| item.rsync_uri.as_str())
            .collect::<std::collections::BTreeSet<_>>();
        for trust_anchor in &self.trust_anchors {
            if object_uris.contains(trust_anchor.ta_rsync_uri.as_str()) {
                return Err(format!(
                    "CIR.objects must not include trust anchor URI {}",
                    trust_anchor.ta_rsync_uri
                ));
            }
        }
        let fresh_object_uris = self
            .fresh_validated_objects
            .iter()
            .map(|item| item.rsync_uri.as_str())
            .collect::<std::collections::BTreeSet<_>>();
        for item in &self.fresh_rejected_objects {
            if !fresh_object_uris.contains(item.object_uri.as_str()) {
                return Err(format!(
                    "CIR.freshRejectedObjects URI must exist in freshValidatedObjects: {}",
                    item.object_uri
                ));
            }
        }
        let cached_object_uris = self
            .cached_validated_objects
            .iter()
            .map(|item| item.rsync_uri.as_str())
            .collect::<std::collections::BTreeSet<_>>();
        for item in &self.cached_rejected_objects {
            if !cached_object_uris.contains(item.object_uri.as_str()) {
                return Err(format!(
                    "CIR.cachedRejectedObjects URI must exist in cachedValidatedObjects: {}",
                    item.object_uri
                ));
            }
        }
        if self.trust_anchors.is_empty() {
            return Err("CIR.trustAnchors must be non-empty".into());
        }
        for (label, digest) in [
            ("CIR.objectListSha256", &self.object_list_sha256),
            ("CIR.freshObjectListSha256", &self.fresh_object_list_sha256),
            (
                "CIR.cachedObjectListSha256",
                &self.cached_object_list_sha256,
            ),
            ("CIR.rejectListSha256", &self.reject_list_sha256),
            ("CIR.freshRejectListSha256", &self.fresh_reject_list_sha256),
            (
                "CIR.cachedRejectListSha256",
                &self.cached_reject_list_sha256,
            ),
        ] {
            if digest.len() != DIGEST_LEN_SHA256 {
                return Err(format!(
                    "{label} must be {DIGEST_LEN_SHA256} bytes, got {}",
                    digest.len()
                ));
            }
        }
        for object in self.validated_objects() {
            object.validate()?;
        }
        for trust_anchor in &self.trust_anchors {
            trust_anchor.validate()?;
        }
        for item in self.rejected_objects() {
            item.validate()?;
        }
        let expected_fresh_object_digest =
            compute_object_list_sha256(self.fresh_validated_objects.iter());
        if self.fresh_object_list_sha256 != expected_fresh_object_digest {
            return Err("CIR.freshObjectListSha256 does not match freshValidatedObjects".into());
        }
        let expected_cached_object_digest =
            compute_object_list_sha256(self.cached_validated_objects.iter());
        if self.cached_object_list_sha256 != expected_cached_object_digest {
            return Err("CIR.cachedObjectListSha256 does not match cachedValidatedObjects".into());
        }
        let expected_object_digest = compute_sectioned_object_list_sha256(
            self.fresh_validated_objects
                .iter()
                .map(|object| ("fresh", object))
                .chain(
                    self.cached_validated_objects
                        .iter()
                        .map(|object| ("cached", object)),
                ),
        );
        if self.object_list_sha256 != expected_object_digest {
            return Err("CIR.objectListSha256 does not match fresh/cached objects".into());
        }
        let expected_fresh_reject_digest = compute_reject_list_sha256(
            self.fresh_rejected_objects
                .iter()
                .map(|item| item.object_uri.as_str()),
        );
        if self.fresh_reject_list_sha256 != expected_fresh_reject_digest {
            return Err("CIR.freshRejectListSha256 does not match freshRejectedObjects".into());
        }
        let expected_cached_reject_digest = compute_reject_list_sha256(
            self.cached_rejected_objects
                .iter()
                .map(|item| item.object_uri.as_str()),
        );
        if self.cached_reject_list_sha256 != expected_cached_reject_digest {
            return Err("CIR.cachedRejectListSha256 does not match cachedRejectedObjects".into());
        }
        let mut all_rejected_uris = self
            .rejected_objects()
            .map(|item| item.object_uri.as_str())
            .collect::<Vec<_>>();
        all_rejected_uris.sort_unstable();
        all_rejected_uris.dedup();
        let expected_digest = compute_reject_list_sha256(all_rejected_uris.iter().copied());
        if self.reject_list_sha256 != expected_digest {
            return Err("CIR.rejectListSha256 does not match fresh/cached rejectedObjects".into());
        }
        Ok(())
    }

    pub fn validated_objects(&self) -> impl Iterator<Item = &CirObject> {
        self.fresh_validated_objects
            .iter()
            .chain(self.cached_validated_objects.iter())
    }

    pub fn rejected_objects(&self) -> impl Iterator<Item = &CirRejectedObject> {
        self.fresh_rejected_objects
            .iter()
            .chain(self.cached_rejected_objects.iter())
    }

    pub fn validated_object_count(&self) -> usize {
        self.fresh_validated_objects.len() + self.cached_validated_objects.len()
    }

    pub fn rejected_object_count(&self) -> usize {
        self.fresh_rejected_objects.len() + self.cached_rejected_objects.len()
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirObject {
    pub rsync_uri: String,
    pub sha256: Vec<u8>,
}

impl CirObject {
    pub fn validate(&self) -> Result<(), String> {
        if !self.rsync_uri.starts_with("rsync://") {
            return Err(format!(
                "CirObject.rsync_uri must start with rsync://, got {}",
                self.rsync_uri
            ));
        }
        if self.sha256.len() != DIGEST_LEN_SHA256 {
            return Err(format!(
                "CirObject.sha256 must be {DIGEST_LEN_SHA256} bytes, got {}",
                self.sha256.len()
            ));
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirTrustAnchor {
    pub ta_rsync_uri: String,
    pub tal_uri: String,
    pub tal_bytes: Vec<u8>,
    pub ta_certificate_der: Vec<u8>,
    pub ta_certificate_sha256: Vec<u8>,
}

impl CirTrustAnchor {
    pub fn validate(&self) -> Result<(), String> {
        if !self.ta_rsync_uri.starts_with("rsync://") {
            return Err(format!(
                "CirTrustAnchor.ta_rsync_uri must start with rsync://, got {}",
                self.ta_rsync_uri
            ));
        }
        if !(self.tal_uri.starts_with("https://") || self.tal_uri.starts_with("http://")) {
            return Err(format!(
                "CirTrustAnchor.tal_uri must start with http:// or https://, got {}",
                self.tal_uri
            ));
        }
        if self.tal_bytes.is_empty() {
            return Err("CirTrustAnchor.tal_bytes must be non-empty".into());
        }
        let tal = crate::data_model::tal::Tal::decode_bytes(&self.tal_bytes)
            .map_err(|e| format!("CirTrustAnchor.tal_bytes must decode as TAL: {e}"))?;
        if !tal
            .ta_uris
            .iter()
            .any(|uri| uri.as_str() == self.ta_rsync_uri)
        {
            return Err(format!(
                "CirTrustAnchor.ta_rsync_uri must be listed in TAL bytes: {}",
                self.ta_rsync_uri
            ));
        }
        if self.ta_certificate_der.is_empty() {
            return Err("CirTrustAnchor.ta_certificate_der must be non-empty".into());
        }
        if self.ta_certificate_sha256.len() != DIGEST_LEN_SHA256 {
            return Err(format!(
                "CirTrustAnchor.ta_certificate_sha256 must be {DIGEST_LEN_SHA256} bytes, got {}",
                self.ta_certificate_sha256.len()
            ));
        }
        let expected = sha256(&self.ta_certificate_der);
        if self.ta_certificate_sha256 != expected {
            return Err("CirTrustAnchor.ta_certificate_sha256 does not match DER bytes".into());
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirRejectedObject {
    pub object_uri: String,
    pub reason: Option<String>,
}

impl CirRejectedObject {
    pub fn validate(&self) -> Result<(), String> {
        if !self.object_uri.starts_with("rsync://") {
            return Err(format!(
                "CirRejectedObject.object_uri must start with rsync://, got {}",
                self.object_uri
            ));
        }
        Ok(())
    }
}

pub fn compute_reject_list_sha256<'a>(uris: impl IntoIterator<Item = &'a str>) -> Vec<u8> {
    let mut body = Vec::new();
    for uri in uris {
        let bytes = uri.as_bytes();
        body.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
        body.extend_from_slice(bytes);
    }
    sha256(&body)
}

pub fn compute_object_list_sha256<'a>(objects: impl IntoIterator<Item = &'a CirObject>) -> Vec<u8> {
    let mut body = Vec::new();
    for object in objects {
        append_digest_string(&mut body, &object.rsync_uri);
        body.extend_from_slice(&object.sha256);
    }
    sha256(&body)
}

pub fn compute_sectioned_object_list_sha256<'a>(
    objects: impl IntoIterator<Item = (&'a str, &'a CirObject)>,
) -> Vec<u8> {
    let mut body = Vec::new();
    for (section, object) in objects {
        append_digest_string(&mut body, section);
        append_digest_string(&mut body, &object.rsync_uri);
        body.extend_from_slice(&object.sha256);
    }
    sha256(&body)
}

pub fn sha256(bytes: &[u8]) -> Vec<u8> {
    use sha2::Digest;

    sha2::Sha256::digest(bytes).to_vec()
}

fn append_digest_string(body: &mut Vec<u8>, value: &str) {
    let bytes = value.as_bytes();
    body.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
    body.extend_from_slice(bytes);
}

fn validate_sorted_unique_strings<'a>(
    items: impl IntoIterator<Item = &'a str>,
    message: &str,
) -> Result<(), String> {
    let mut prev: Option<&'a str> = None;
    for key in items {
        if let Some(prev_key) = prev
            && key <= prev_key
        {
            return Err(message.into());
        }
        prev = Some(key);
    }
    Ok(())
}
