use std::collections::BTreeMap;

use crate::ccr::build::{
    build_aspa_payload_state, build_roa_payload_state, build_router_key_state_from_runtime,
    build_trust_anchor_state,
};
use crate::ccr::encode::encode_manifest_state_payload_der;
use crate::ccr::hash::compute_state_hash;
use crate::ccr::manifest_location::select_manifest_signed_object_location_from_der;
use crate::ccr::model::{
    CcrDigestAlgorithm, ManifestInstance, ManifestState, RpkiCanonicalCacheRepresentation,
};
use crate::data_model::common::BigUnsigned;
use crate::data_model::ta::TrustAnchor;
use crate::storage::VcirCcrManifestProjection;
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CcrManifestContribution {
    pub manifest_rsync_uri: String,
    pub hash: Vec<u8>,
    pub size: u64,
    pub aki: Vec<u8>,
    pub manifest_number_be: Vec<u8>,
    pub this_update: time::OffsetDateTime,
    pub locations_der: Vec<Vec<u8>>,
    pub subordinate_skis: Vec<Vec<u8>>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CcrAccumulatorMemoryStats {
    pub trust_anchor_count: u64,
    pub manifest_count: u64,
    pub estimated_heap_bytes: u64,
    pub string_bytes: u64,
    pub string_capacity_bytes: u64,
    pub vec_payload_bytes: u64,
    pub vec_capacity_bytes: u64,
    pub locations_der_count: u64,
    pub subordinate_ski_count: u64,
    pub btree_key_capacity_bytes: u64,
    pub btree_entry_shallow_bytes: u64,
}

impl CcrManifestContribution {
    fn from_projection(projection: &VcirCcrManifestProjection) -> Result<Self, String> {
        let this_update = projection
            .manifest_this_update
            .parse()
            .map_err(|e| format!("parse projection manifest_this_update failed: {e}"))?;
        Ok(Self {
            manifest_rsync_uri: projection.manifest_rsync_uri.clone(),
            hash: projection.manifest_sha256.clone(),
            size: projection.manifest_size,
            aki: projection.manifest_ee_aki.clone(),
            manifest_number_be: projection.manifest_number_be.clone(),
            this_update,
            locations_der: vec![select_manifest_signed_object_location_from_der(
                &projection.manifest_rsync_uri,
                &projection.manifest_sia_locations_der,
            )?],
            subordinate_skis: projection.subordinate_skis.clone(),
        })
    }

    fn to_manifest_instance(&self) -> ManifestInstance {
        ManifestInstance {
            hash: self.hash.clone(),
            size: self.size,
            aki: self.aki.clone(),
            manifest_number: BigUnsigned {
                bytes_be: self.manifest_number_be.clone(),
            },
            this_update: self.this_update,
            locations: self.locations_der.clone(),
            subordinates: self.subordinate_skis.clone(),
        }
    }

    fn add_memory_stats(&self, stats: &mut CcrAccumulatorMemoryStats) {
        stats.string_bytes += self.manifest_rsync_uri.len() as u64;
        stats.string_capacity_bytes += self.manifest_rsync_uri.capacity() as u64;
        stats.estimated_heap_bytes += self.manifest_rsync_uri.capacity() as u64;

        add_vec_stats(&self.hash, stats);
        add_vec_stats(&self.aki, stats);
        add_vec_stats(&self.manifest_number_be, stats);
        add_vec_of_vec_stats(&self.locations_der, stats);
        add_vec_of_vec_stats(&self.subordinate_skis, stats);
        stats.locations_der_count += self.locations_der.len() as u64;
        stats.subordinate_ski_count += self.subordinate_skis.len() as u64;
    }
}

fn add_vec_stats(value: &Vec<u8>, stats: &mut CcrAccumulatorMemoryStats) {
    stats.vec_payload_bytes += value.len() as u64;
    stats.vec_capacity_bytes += value.capacity() as u64;
    stats.estimated_heap_bytes += value.capacity() as u64;
}

fn add_vec_of_vec_stats(values: &Vec<Vec<u8>>, stats: &mut CcrAccumulatorMemoryStats) {
    let outer_capacity = values.capacity() * std::mem::size_of::<Vec<u8>>();
    stats.vec_payload_bytes += (values.len() * std::mem::size_of::<Vec<u8>>()) as u64;
    stats.vec_capacity_bytes += outer_capacity as u64;
    stats.estimated_heap_bytes += outer_capacity as u64;
    for value in values {
        add_vec_stats(value, stats);
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CcrAccumulator {
    trust_anchors: Vec<TrustAnchor>,
    manifests_by_hash: BTreeMap<Vec<u8>, CcrManifestContribution>,
    most_recent_update: time::OffsetDateTime,
}

impl CcrAccumulator {
    pub fn new(trust_anchors: Vec<TrustAnchor>) -> Self {
        Self {
            trust_anchors,
            manifests_by_hash: BTreeMap::new(),
            most_recent_update: time::OffsetDateTime::UNIX_EPOCH,
        }
    }

    pub fn append_manifest_projection(
        &mut self,
        projection: &VcirCcrManifestProjection,
    ) -> Result<(), String> {
        let contribution = CcrManifestContribution::from_projection(projection)?;
        match self.manifests_by_hash.get(contribution.hash.as_slice()) {
            Some(existing) if existing != &contribution => {
                return Err(format!(
                    "duplicate manifest hash with conflicting content for URI: {}",
                    contribution.manifest_rsync_uri
                ));
            }
            Some(_) => {}
            None => {
                self.manifests_by_hash
                    .insert(contribution.hash.clone(), contribution.clone());
            }
        }
        if contribution.this_update > self.most_recent_update {
            self.most_recent_update = contribution.this_update;
        }
        Ok(())
    }

    pub fn finish(
        &self,
        produced_at: time::OffsetDateTime,
        vrps: &[Vrp],
        aspas: &[AspaAttestation],
        router_keys: &[RouterKeyPayload],
    ) -> Result<RpkiCanonicalCacheRepresentation, String> {
        let manifest_instances = self
            .manifests_by_hash
            .values()
            .map(CcrManifestContribution::to_manifest_instance)
            .collect::<Vec<_>>();
        let manifest_payload_der = encode_manifest_state_payload_der(&manifest_instances)
            .map_err(|e| format!("manifest state encoding failed: {e}"))?;
        let manifest_state = ManifestState {
            mis: manifest_instances,
            most_recent_update: self.most_recent_update,
            hash: compute_state_hash(&manifest_payload_der),
        };
        let vrp_state = build_roa_payload_state(vrps).map_err(|e| e.to_string())?;
        let aspa_state = build_aspa_payload_state(aspas).map_err(|e| e.to_string())?;
        let ta_state = build_trust_anchor_state(&self.trust_anchors).map_err(|e| e.to_string())?;
        let router_key_state =
            build_router_key_state_from_runtime(router_keys).map_err(|e| e.to_string())?;
        Ok(RpkiCanonicalCacheRepresentation {
            version: 0,
            hash_alg: CcrDigestAlgorithm::Sha256,
            produced_at,
            mfts: Some(manifest_state),
            vrps: Some(vrp_state),
            vaps: Some(aspa_state),
            tas: Some(ta_state),
            rks: Some(router_key_state),
        })
    }

    pub fn manifest_count(&self) -> usize {
        self.manifests_by_hash.len()
    }

    pub fn memory_stats(&self) -> CcrAccumulatorMemoryStats {
        let mut stats = CcrAccumulatorMemoryStats {
            trust_anchor_count: self.trust_anchors.len() as u64,
            manifest_count: self.manifests_by_hash.len() as u64,
            ..CcrAccumulatorMemoryStats::default()
        };
        stats.estimated_heap_bytes +=
            (self.trust_anchors.capacity() * std::mem::size_of::<TrustAnchor>()) as u64;
        for trust_anchor in &self.trust_anchors {
            add_vec_stats(&trust_anchor.tal.raw, &mut stats);
            add_vec_of_string_stats(&trust_anchor.tal.comments, &mut stats);
            stats.vec_payload_bytes +=
                (trust_anchor.tal.ta_uris.len() * std::mem::size_of::<url::Url>()) as u64;
            stats.vec_capacity_bytes +=
                (trust_anchor.tal.ta_uris.capacity() * std::mem::size_of::<url::Url>()) as u64;
            stats.estimated_heap_bytes +=
                (trust_anchor.tal.ta_uris.capacity() * std::mem::size_of::<url::Url>()) as u64;
            for uri in &trust_anchor.tal.ta_uris {
                stats.string_bytes += uri.as_str().len() as u64;
                stats.string_capacity_bytes += uri.as_str().len() as u64;
                stats.estimated_heap_bytes += uri.as_str().len() as u64;
            }
            add_vec_stats(&trust_anchor.tal.subject_public_key_info_der, &mut stats);
            add_vec_stats(&trust_anchor.ta_certificate.raw_der, &mut stats);
            if let Some(uri) = &trust_anchor.resolved_ta_uri {
                stats.string_bytes += uri.as_str().len() as u64;
                stats.string_capacity_bytes += uri.as_str().len() as u64;
                stats.estimated_heap_bytes += uri.as_str().len() as u64;
            }
        }

        stats.btree_entry_shallow_bytes = (self.manifests_by_hash.len()
            * (std::mem::size_of::<Vec<u8>>() + std::mem::size_of::<CcrManifestContribution>()))
            as u64;
        stats.estimated_heap_bytes += stats.btree_entry_shallow_bytes;
        for (key, contribution) in &self.manifests_by_hash {
            stats.btree_key_capacity_bytes += key.capacity() as u64;
            stats.estimated_heap_bytes += key.capacity() as u64;
            contribution.add_memory_stats(&mut stats);
        }
        stats
    }
}

fn add_vec_of_string_stats(values: &Vec<String>, stats: &mut CcrAccumulatorMemoryStats) {
    let outer_capacity = values.capacity() * std::mem::size_of::<String>();
    stats.vec_payload_bytes += (values.len() * std::mem::size_of::<String>()) as u64;
    stats.vec_capacity_bytes += outer_capacity as u64;
    stats.estimated_heap_bytes += outer_capacity as u64;
    for value in values {
        stats.string_bytes += value.len() as u64;
        stats.string_capacity_bytes += value.capacity() as u64;
        stats.estimated_heap_bytes += value.capacity() as u64;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ccr::export::build_ccr_from_run;
    use crate::ccr::verify::verify_content_info;
    use crate::data_model::manifest::ManifestObject;
    use crate::data_model::oid::{OID_AD_RPKI_NOTIFY, OID_AD_SIGNED_OBJECT};
    use crate::data_model::rc::{AccessDescription, SubjectInfoAccess};
    use crate::data_model::roa::{IpPrefix, RoaAfi};
    use crate::data_model::ta::TrustAnchor;
    use crate::data_model::tal::Tal;
    use crate::storage::{
        PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
        VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
        VcirCcrManifestProjection, VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact,
        VcirSummary,
    };
    use sha2::Digest;

    fn sample_trust_anchor() -> TrustAnchor {
        let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        let tal_bytes = std::fs::read(base.join("tests/fixtures/tal/apnic-rfc7730-https.tal"))
            .expect("read tal");
        let ta_der = std::fs::read(base.join("tests/fixtures/ta/apnic-ta.cer")).expect("read ta");
        let tal = Tal::decode_bytes(&tal_bytes).expect("decode tal");
        TrustAnchor::bind_der(tal, &ta_der, None).expect("bind ta")
    }

    fn sample_vcir_and_manifest(
        store: &RocksStore,
    ) -> (
        ValidatedCaInstanceResult,
        Vec<Vrp>,
        Vec<AspaAttestation>,
        Vec<RouterKeyPayload>,
    ) {
        let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        let manifest_der = std::fs::read(
            base.join(
                "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
            ),
        )
        .expect("read manifest");
        let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest");
        let manifest_uri = match manifest.signed_object.signed_data.certificates[0]
            .resource_cert
            .tbs
            .extensions
            .subject_info_access
            .as_ref()
            .expect("manifest sia")
        {
            SubjectInfoAccess::Ee(ee_sia) => ee_sia
                .access_descriptions
                .iter()
                .find(|ad| {
                    ad.access_method_oid == OID_AD_SIGNED_OBJECT
                        && ad.access_location.starts_with("rsync://")
                })
                .expect("manifest rsync signedObject")
                .access_location
                .clone(),
            SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
        };
        let manifest_hash = hex::encode(sha2::Sha256::digest(&manifest_der));
        let mut raw = RawByHashEntry::from_bytes(manifest_hash.clone(), manifest_der.clone());
        raw.origin_uris.push(manifest_uri.clone());
        raw.object_type = Some("mft".to_string());
        raw.encoding = Some("der".to_string());
        store.put_raw_by_hash_entry(&raw).expect("put raw");

        let projection = VcirCcrManifestProjection {
            manifest_rsync_uri: manifest_uri.clone(),
            manifest_sha256: sha2::Sha256::digest(&manifest_der).to_vec(),
            manifest_size: manifest_der.len() as u64,
            manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
                .resource_cert
                .tbs
                .extensions
                .authority_key_identifier
                .clone()
                .expect("manifest aki"),
            manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
            manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
            manifest_sia_locations_der: match manifest.signed_object.signed_data.certificates[0]
                .resource_cert
                .tbs
                .extensions
                .subject_info_access
                .as_ref()
                .expect("manifest sia")
            {
                SubjectInfoAccess::Ee(ee_sia) => vec![
                    crate::ccr::manifest_location::select_manifest_signed_object_location(
                        &manifest_uri,
                        &ee_sia.access_descriptions,
                    )
                    .expect("select manifest signedObject"),
                ],
                SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
            },
            subordinate_skis: vec![vec![0x33; 20]],
        };

        let vcir = ValidatedCaInstanceResult {
            manifest_rsync_uri: manifest_uri.clone(),
            parent_manifest_rsync_uri: None,
            tal_id: "apnic".to_string(),
            ca_subject_name: "CN=test".to_string(),
            ca_ski: "11".repeat(20),
            issuer_ski: "22".repeat(20),
            last_successful_validation_time: PackTime::from_utc_offset_datetime(
                manifest.manifest.this_update,
            ),
            current_manifest_rsync_uri: manifest_uri.clone(),
            current_crl_rsync_uri: format!("{manifest_uri}.crl"),
            validated_manifest_meta: ValidatedManifestMeta {
                validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
                validated_manifest_this_update: PackTime::from_utc_offset_datetime(
                    manifest.manifest.this_update,
                ),
                validated_manifest_next_update: PackTime::from_utc_offset_datetime(
                    manifest.manifest.next_update,
                ),
            },
            ccr_manifest_projection: projection.clone(),
            instance_gate: VcirInstanceGate {
                manifest_next_update: PackTime::from_utc_offset_datetime(
                    manifest.manifest.next_update,
                ),
                current_crl_next_update: PackTime::from_utc_offset_datetime(
                    manifest.manifest.next_update,
                ),
                self_ca_not_after: PackTime::from_utc_offset_datetime(
                    manifest.manifest.next_update,
                ),
                instance_effective_until: PackTime::from_utc_offset_datetime(
                    manifest.manifest.next_update,
                ),
            },
            child_entries: vec![VcirChildEntry {
                child_manifest_rsync_uri: "rsync://example.test/repo/child.mft".to_string(),
                child_cert_rsync_uri: "rsync://example.test/repo/child.cer".to_string(),
                child_cert_hash: "aa".repeat(32),
                child_ski: "33".repeat(20),
                child_rsync_base_uri: "rsync://example.test/repo/".to_string(),
                child_publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
                child_rrdp_notification_uri: None,
                child_effective_ip_resources: None,
                child_effective_as_resources: None,
                accepted_at_validation_time: PackTime::from_utc_offset_datetime(
                    manifest.manifest.this_update,
                ),
            }],
            local_outputs: Vec::new(),
            related_artifacts: vec![VcirRelatedArtifact {
                artifact_role: VcirArtifactRole::Manifest,
                artifact_kind: VcirArtifactKind::Mft,
                uri: Some(manifest_uri.clone()),
                sha256: manifest_hash,
                object_type: Some("mft".to_string()),
                validation_status: VcirArtifactValidationStatus::Accepted,
                reject_reason: None,
            }],
            summary: VcirSummary {
                local_vrp_count: 0,
                local_aspa_count: 0,
                local_router_key_count: 0,
                child_count: 1,
                accepted_object_count: 1,
                rejected_object_count: 0,
            },
            audit_summary: VcirAuditSummary {
                failed_fetch_eligible: true,
                last_failed_fetch_reason: None,
                warning_count: 0,
                audit_flags: Vec::new(),
            },
        };

        let vrps = vec![Vrp {
            asn: 64496,
            prefix: IpPrefix {
                afi: RoaAfi::Ipv4,
                prefix_len: 8,
                addr: [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            },
            max_length: 8,
        }];
        let aspas = vec![AspaAttestation {
            customer_as_id: 64496,
            provider_as_ids: vec![64497],
        }];
        let router_keys = vec![RouterKeyPayload {
            as_id: 64496,
            ski: vec![0x11; 20],
            spki_der: vec![0x30, 0x00],
            source_object_uri: "rsync://example.test/repo/router.cer".to_string(),
            source_object_hash: hex::encode([0x11; 32]),
            source_ee_cert_hash: hex::encode([0x11; 32]),
            item_effective_until: PackTime::from_utc_offset_datetime(
                time::OffsetDateTime::now_utc() + time::Duration::hours(1),
            ),
        }];
        (vcir, vrps, aspas, router_keys)
    }

    #[test]
    fn accumulator_finish_matches_builder_on_fresh_vcir_inputs() {
        let td = tempfile::tempdir().expect("tempdir");
        let store = RocksStore::open(td.path()).expect("open rocksdb");
        let (vcir, vrps, aspas, router_keys) = sample_vcir_and_manifest(&store);
        store.put_vcir(&vcir).expect("put vcir");
        let trust_anchor = sample_trust_anchor();

        let builder_ccr = build_ccr_from_run(
            &store,
            &[trust_anchor.clone()],
            &vrps,
            &aspas,
            &router_keys,
            time::OffsetDateTime::now_utc(),
        )
        .expect("build ccr from run");

        let mut accumulator = CcrAccumulator::new(vec![trust_anchor]);
        accumulator
            .append_manifest_projection(&vcir.ccr_manifest_projection)
            .expect("append manifest projection");
        let accumulated_ccr = accumulator
            .finish(time::OffsetDateTime::now_utc(), &vrps, &aspas, &router_keys)
            .expect("finish accumulator");

        assert_eq!(builder_ccr.mfts, accumulated_ccr.mfts);
        assert_eq!(builder_ccr.vrps, accumulated_ccr.vrps);
        assert_eq!(builder_ccr.vaps, accumulated_ccr.vaps);
        assert_eq!(builder_ccr.tas, accumulated_ccr.tas);
        assert_eq!(builder_ccr.rks, accumulated_ccr.rks);
        let ci = crate::ccr::model::CcrContentInfo::new(accumulated_ccr);
        verify_content_info(&ci).expect("verify accumulated ccr");
    }

    #[test]
    fn accumulator_sanitizes_historical_projection_with_rpki_notify() {
        let td = tempfile::tempdir().expect("tempdir");
        let store = RocksStore::open(td.path()).expect("open rocksdb");
        let (mut vcir, _, _, _) = sample_vcir_and_manifest(&store);
        let notify_der =
            crate::ccr::manifest_location::encode_access_description_der(&AccessDescription {
                access_method_oid: OID_AD_RPKI_NOTIFY.to_string(),
                access_location: "https://rrdp.example.test/notification.xml".to_string(),
            })
            .expect("encode rpkiNotify");
        let expected_location = vcir.ccr_manifest_projection.manifest_sia_locations_der[0].clone();
        vcir.ccr_manifest_projection
            .manifest_sia_locations_der
            .push(notify_der);

        let mut accumulator = CcrAccumulator::new(Vec::new());
        accumulator
            .append_manifest_projection(&vcir.ccr_manifest_projection)
            .expect("sanitize historical projection");
        let contribution = accumulator
            .manifests_by_hash
            .values()
            .next()
            .expect("manifest contribution");
        assert_eq!(contribution.locations_der, vec![expected_location]);
    }
}
