use crate::ccr::build::{
    CcrBuildError, ManifestStateBuildBreakdown, build_aspa_payload_state,
    build_manifest_state_from_vcirs_with_breakdown, build_roa_payload_state,
    build_router_key_state_from_runtime, build_trust_anchor_state,
};
use crate::ccr::encode::{CcrEncodeError, encode_content_info};
use crate::ccr::model::{CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation};
use crate::data_model::ta::TrustAnchor;
use crate::storage::RocksStore;
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
use serde::Serialize;
use std::path::Path;

#[derive(Debug, thiserror::Error)]
pub enum CcrExportError {
    #[error("list VCIRs failed: {0}")]
    ListVcirs(String),

    #[error("build CCR state failed: {0}")]
    Build(#[from] CcrBuildError),

    #[error("encode CCR failed: {0}")]
    Encode(#[from] CcrEncodeError),

    #[error("write CCR file failed: {0}: {1}")]
    Write(String, String),
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct CcrBuildBreakdown {
    pub vcir_count: usize,
    pub list_vcirs_ms: u64,
    pub manifest_state_ms: u64,
    pub manifest_state_breakdown: ManifestStateBuildBreakdown,
    pub roa_payload_state_ms: u64,
    pub aspa_payload_state_ms: u64,
    pub trust_anchor_state_ms: u64,
    pub router_key_state_ms: u64,
    pub total_ms: u64,
}

pub fn build_ccr_from_run(
    store: &RocksStore,
    trust_anchors: &[TrustAnchor],
    vrps: &[Vrp],
    aspas: &[AspaAttestation],
    router_keys: &[RouterKeyPayload],
    produced_at: time::OffsetDateTime,
) -> Result<RpkiCanonicalCacheRepresentation, CcrExportError> {
    build_ccr_from_run_with_breakdown(store, trust_anchors, vrps, aspas, router_keys, produced_at)
        .map(|(ccr, _)| ccr)
}

pub fn build_ccr_from_run_with_breakdown(
    store: &RocksStore,
    trust_anchors: &[TrustAnchor],
    vrps: &[Vrp],
    aspas: &[AspaAttestation],
    router_keys: &[RouterKeyPayload],
    produced_at: time::OffsetDateTime,
) -> Result<(RpkiCanonicalCacheRepresentation, CcrBuildBreakdown), CcrExportError> {
    let total_started = std::time::Instant::now();
    let mut breakdown = CcrBuildBreakdown::default();

    let started = std::time::Instant::now();
    let vcirs = store
        .list_vcirs()
        .map_err(|e| CcrExportError::ListVcirs(e.to_string()))?;
    breakdown.list_vcirs_ms = started.elapsed().as_millis() as u64;
    breakdown.vcir_count = vcirs.len();

    let started = std::time::Instant::now();
    let (mfts, manifest_state_breakdown) =
        build_manifest_state_from_vcirs_with_breakdown(store, &vcirs)?;
    breakdown.manifest_state_ms = started.elapsed().as_millis() as u64;
    breakdown.manifest_state_breakdown = manifest_state_breakdown;

    let started = std::time::Instant::now();
    let vrps = build_roa_payload_state(vrps)?;
    breakdown.roa_payload_state_ms = started.elapsed().as_millis() as u64;

    let started = std::time::Instant::now();
    let vaps = build_aspa_payload_state(aspas)?;
    breakdown.aspa_payload_state_ms = started.elapsed().as_millis() as u64;

    let started = std::time::Instant::now();
    let tas = build_trust_anchor_state(trust_anchors)?;
    breakdown.trust_anchor_state_ms = started.elapsed().as_millis() as u64;

    let started = std::time::Instant::now();
    let rks = build_router_key_state_from_runtime(router_keys)?;
    breakdown.router_key_state_ms = started.elapsed().as_millis() as u64;

    let ccr = RpkiCanonicalCacheRepresentation {
        version: 0,
        hash_alg: CcrDigestAlgorithm::Sha256,
        produced_at,
        mfts: Some(mfts),
        vrps: Some(vrps),
        vaps: Some(vaps),
        tas: Some(tas),
        rks: Some(rks),
    };
    breakdown.total_ms = total_started.elapsed().as_millis() as u64;

    Ok((ccr, breakdown))
}

pub fn write_ccr_file(
    path: &Path,
    ccr: &RpkiCanonicalCacheRepresentation,
) -> Result<(), CcrExportError> {
    let der = encode_content_info(&CcrContentInfo::new(ccr.clone()))?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| CcrExportError::Write(path.display().to_string(), e.to_string()))?;
    }
    std::fs::write(path, der)
        .map_err(|e| CcrExportError::Write(path.display().to_string(), e.to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ccr::decode::decode_content_info;
    use crate::data_model::manifest::ManifestObject;
    use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
    use crate::data_model::rc::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 crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
    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_raw(store: &RocksStore) -> ValidatedCaInstanceResult {
        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 hash = hex::encode(sha2::Sha256::digest(&manifest_der));
        let mut raw = RawByHashEntry::from_bytes(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]],
        };
        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: "rsync://example.test/repo/current.crl".to_string(),
            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,
            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),
                sha256: 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(),
            },
        }
    }

    #[test]
    fn build_and_write_ccr_from_run_exports_der_content_info() {
        let td = tempfile::tempdir().expect("tempdir");
        let db_path = td.path().join("db");
        let store = RocksStore::open(&db_path).expect("open rocksdb");
        let vcir = sample_vcir_and_raw(&store);
        store.put_vcir(&vcir).expect("put vcir");
        let trust_anchor = sample_trust_anchor();
        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),
            ),
        }];
        let ccr = build_ccr_from_run(
            &store,
            &[trust_anchor],
            &vrps,
            &aspas,
            &router_keys,
            time::OffsetDateTime::now_utc(),
        )
        .expect("build ccr");
        assert!(ccr.mfts.is_some());
        assert!(ccr.vrps.is_some());
        assert!(ccr.vaps.is_some());
        assert!(ccr.tas.is_some());
        assert!(ccr.rks.is_some());
        assert_eq!(ccr.rks.as_ref().unwrap().rksets.len(), 1);
        let out = td.path().join("out/example.ccr");
        write_ccr_file(&out, &ccr).expect("write ccr");
        let der = std::fs::read(&out).expect("read ccr file");
        let decoded = decode_content_info(&der).expect("decode ccr");
        assert_eq!(decoded.content.version, 0);
        assert!(decoded.content.mfts.is_some());
    }
}
