use crate::cir::model::{
    CIR_VERSION_V4, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject,
    CirTrustAnchor,
};
use crate::data_model::oid::OID_SHA256_RAW;

#[derive(Debug, thiserror::Error)]
pub enum CirEncodeError {
    #[error("CIR model validation failed: {0}")]
    Validate(String),
}

pub fn encode_cir(cir: &CanonicalInputRepresentation) -> Result<Vec<u8>, CirEncodeError> {
    cir.validate().map_err(CirEncodeError::Validate)?;
    Ok(encode_sequence(&[
        encode_integer_u32(CIR_VERSION_V4),
        encode_oid(match cir.hash_alg {
            CirHashAlgorithm::Sha256 => OID_SHA256_RAW,
        }),
        encode_generalized_time(cir.validation_time),
        encode_sequence(
            &cir.fresh_validated_objects
                .iter()
                .map(encode_object)
                .collect::<Result<Vec<_>, _>>()?,
        ),
        encode_sequence(
            &cir.cached_validated_objects
                .iter()
                .map(encode_object)
                .collect::<Result<Vec<_>, _>>()?,
        ),
        encode_sequence(
            &cir.trust_anchors
                .iter()
                .map(encode_trust_anchor)
                .collect::<Result<Vec<_>, _>>()?,
        ),
        encode_octet_string(&cir.object_list_sha256),
        encode_octet_string(&cir.fresh_object_list_sha256),
        encode_octet_string(&cir.cached_object_list_sha256),
        encode_octet_string(&cir.reject_list_sha256),
        encode_octet_string(&cir.fresh_reject_list_sha256),
        encode_octet_string(&cir.cached_reject_list_sha256),
        encode_sequence(
            &cir.fresh_rejected_objects
                .iter()
                .map(encode_rejected_object)
                .collect::<Result<Vec<_>, _>>()?,
        ),
        encode_sequence(
            &cir.cached_rejected_objects
                .iter()
                .map(encode_rejected_object)
                .collect::<Result<Vec<_>, _>>()?,
        ),
    ]))
}

fn encode_object(object: &CirObject) -> Result<Vec<u8>, CirEncodeError> {
    object.validate().map_err(CirEncodeError::Validate)?;
    Ok(encode_sequence(&[
        encode_ia5_string(object.rsync_uri.as_bytes()),
        encode_octet_string(&object.sha256),
    ]))
}

fn encode_trust_anchor(trust_anchor: &CirTrustAnchor) -> Result<Vec<u8>, CirEncodeError> {
    trust_anchor.validate().map_err(CirEncodeError::Validate)?;
    Ok(encode_sequence(&[
        encode_ia5_string(trust_anchor.ta_rsync_uri.as_bytes()),
        encode_ia5_string(trust_anchor.tal_uri.as_bytes()),
        encode_octet_string(&trust_anchor.tal_bytes),
        encode_octet_string(&trust_anchor.ta_certificate_der),
        encode_octet_string(&trust_anchor.ta_certificate_sha256),
    ]))
}

fn encode_rejected_object(item: &CirRejectedObject) -> Result<Vec<u8>, CirEncodeError> {
    item.validate().map_err(CirEncodeError::Validate)?;
    let mut fields = vec![encode_ia5_string(item.object_uri.as_bytes())];
    if let Some(reason) = &item.reason {
        fields.push(encode_octet_string(reason.as_bytes()));
    }
    Ok(encode_sequence(&fields))
}

fn encode_generalized_time(t: time::OffsetDateTime) -> Vec<u8> {
    let t = t.to_offset(time::UtcOffset::UTC);
    let s = format!(
        "{:04}{:02}{:02}{:02}{:02}{:02}Z",
        t.year(),
        u8::from(t.month()),
        t.day(),
        t.hour(),
        t.minute(),
        t.second()
    );
    encode_tlv(0x18, s.into_bytes())
}

fn encode_integer_u32(v: u32) -> Vec<u8> {
    encode_integer_bytes(unsigned_integer_bytes(v as u64))
}

fn encode_integer_bytes(mut bytes: Vec<u8>) -> Vec<u8> {
    if bytes.is_empty() {
        bytes.push(0);
    }
    if bytes[0] & 0x80 != 0 {
        bytes.insert(0, 0);
    }
    encode_tlv(0x02, bytes)
}

fn unsigned_integer_bytes(v: u64) -> Vec<u8> {
    if v == 0 {
        return vec![0];
    }
    let mut out = Vec::new();
    let mut n = v;
    while n > 0 {
        out.push((n & 0xFF) as u8);
        n >>= 8;
    }
    out.reverse();
    out
}

fn encode_oid(raw_body: &[u8]) -> Vec<u8> {
    encode_tlv(0x06, raw_body.to_vec())
}

fn encode_ia5_string(bytes: &[u8]) -> Vec<u8> {
    encode_tlv(0x16, bytes.to_vec())
}

fn encode_octet_string(bytes: &[u8]) -> Vec<u8> {
    encode_tlv(0x04, bytes.to_vec())
}

fn encode_sequence(elements: &[Vec<u8>]) -> Vec<u8> {
    let mut body = Vec::new();
    for element in elements {
        body.extend_from_slice(element);
    }
    encode_tlv(0x30, body)
}

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

fn encoded_len_len(len: usize) -> usize {
    if len < 0x80 {
        1
    } else {
        1 + len.to_be_bytes().iter().skip_while(|&&b| b == 0).count()
    }
}

fn encode_len_into(len: usize, out: &mut Vec<u8>) {
    if len < 0x80 {
        out.push(len as u8);
        return;
    }
    let bytes = len.to_be_bytes();
    let first_non_zero = bytes
        .iter()
        .position(|&b| b != 0)
        .unwrap_or(bytes.len() - 1);
    let len_bytes = &bytes[first_non_zero..];
    out.push(0x80 | (len_bytes.len() as u8));
    out.extend_from_slice(len_bytes);
}
