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 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 objects: Vec<CirObject>,
    pub trust_anchors: Vec<CirTrustAnchor>,
    pub reject_list_sha256: Vec<u8>,
    pub rejected_objects: Vec<CirRejectedObject>,
}

impl CanonicalInputRepresentation {
    pub fn validate(&self) -> Result<(), String> {
        if self.version != CIR_VERSION_V3 {
            return Err(format!(
                "CIR version must be {CIR_VERSION_V3}, 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.objects.iter().map(|item| item.rsync_uri.as_str()),
            "CIR.objects 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.rejected_objects
                .iter()
                .map(|item| item.object_uri.as_str()),
            "CIR.rejectedObjects must be sorted by objectUri and unique",
        )?;
        let object_uris = self
            .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
                ));
            }
        }
        if self.trust_anchors.is_empty() {
            return Err("CIR.trustAnchors must be non-empty".into());
        }
        if self.reject_list_sha256.len() != DIGEST_LEN_SHA256 {
            return Err(format!(
                "CIR.rejectListSha256 must be {DIGEST_LEN_SHA256} bytes, got {}",
                self.reject_list_sha256.len()
            ));
        }
        for object in &self.objects {
            object.validate()?;
        }
        for trust_anchor in &self.trust_anchors {
            trust_anchor.validate()?;
        }
        for item in &self.rejected_objects {
            item.validate()?;
        }
        let expected_digest = compute_reject_list_sha256(
            self.rejected_objects
                .iter()
                .map(|item| item.object_uri.as_str()),
        );
        if self.reject_list_sha256 != expected_digest {
            return Err("CIR.rejectListSha256 does not match rejectedObjects".into());
        }
        Ok(())
    }
}

#[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 sha256(bytes: &[u8]) -> Vec<u8> {
    use sha2::Digest;

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

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(())
}
