use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::Path;

use crate::cir::encode::{CirEncodeError, encode_cir};
use crate::cir::model::{
    CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
};
use crate::cir::static_pool::{
    CirStaticPoolError, CirStaticPoolExportSummary, write_bytes_to_static_pool,
    export_hashes_from_store,
};
use crate::data_model::ta::TrustAnchor;
use crate::storage::{RepositoryViewState, RocksStore};

#[derive(Debug, thiserror::Error)]
pub enum CirExportError {
    #[error("list repository_view entries failed: {0}")]
    ListRepositoryView(String),

    #[error("CIR TAL URI must be http(s), got: {0}")]
    InvalidTalUri(String),

    #[error("TAL does not contain any rsync TA URI; CIR replay scheme A requires one")]
    MissingTaRsyncUri,

    #[error("CIR model validation failed: {0}")]
    Validate(String),

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

    #[error("static pool export failed: {0}")]
    StaticPool(#[from] CirStaticPoolError),

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

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirExportSummary {
    pub object_count: usize,
    pub tal_count: usize,
    pub static_pool: CirStaticPoolExportSummary,
}

pub fn build_cir_from_run(
    store: &RocksStore,
    trust_anchor: &TrustAnchor,
    tal_uri: &str,
    validation_time: time::OffsetDateTime,
) -> Result<CanonicalInputRepresentation, CirExportError> {
    if !(tal_uri.starts_with("https://") || tal_uri.starts_with("http://")) {
        return Err(CirExportError::InvalidTalUri(tal_uri.to_string()));
    }

    let entries = store
        .list_repository_view_entries_with_prefix("rsync://")
        .map_err(|e| CirExportError::ListRepositoryView(e.to_string()))?;

    let mut objects: BTreeMap<String, String> = BTreeMap::new();
    for entry in entries {
        if matches!(
            entry.state,
            RepositoryViewState::Present | RepositoryViewState::Replaced
        ) && let Some(hash) = entry.current_hash
        {
            objects.insert(entry.rsync_uri, hash.to_ascii_lowercase());
        }
    }

    let ta_hash = ta_sha256_hex(&trust_anchor.ta_certificate.raw_der);
    let mut saw_rsync_uri = false;
    for uri in &trust_anchor.tal.ta_uris {
        if uri.scheme() == "rsync" {
            saw_rsync_uri = true;
            objects.insert(uri.as_str().to_string(), ta_hash.clone());
        }
    }
    if !saw_rsync_uri {
        return Err(CirExportError::MissingTaRsyncUri);
    }

    let cir = CanonicalInputRepresentation {
        version: CIR_VERSION_V1,
        hash_alg: CirHashAlgorithm::Sha256,
        validation_time: validation_time.to_offset(time::UtcOffset::UTC),
        objects: objects
            .into_iter()
            .map(|(rsync_uri, sha256_hex)| CirObject {
                rsync_uri,
                sha256: hex::decode(sha256_hex).expect("validated hex"),
            })
            .collect(),
        tals: vec![CirTal {
            tal_uri: tal_uri.to_string(),
            tal_bytes: trust_anchor.tal.raw.clone(),
        }],
    };
    cir.validate().map_err(CirExportError::Validate)?;
    Ok(cir)
}

pub fn write_cir_file(path: &Path, cir: &CanonicalInputRepresentation) -> Result<(), CirExportError> {
    let der = encode_cir(cir)?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| CirExportError::Write(path.display().to_string(), e.to_string()))?;
    }
    std::fs::write(path, der)
        .map_err(|e| CirExportError::Write(path.display().to_string(), e.to_string()))
}

pub fn export_cir_static_pool(
    store: &RocksStore,
    static_root: &Path,
    capture_date_utc: time::Date,
    cir: &CanonicalInputRepresentation,
    trust_anchor: &TrustAnchor,
) -> Result<CirStaticPoolExportSummary, CirExportError> {
    let ta_hash = ta_sha256_hex(&trust_anchor.ta_certificate.raw_der);
    let hashes = cir
        .objects
        .iter()
        .map(|item| hex::encode(&item.sha256))
        .filter(|hash| hash != &ta_hash)
        .collect::<Vec<_>>();
    let mut summary = export_hashes_from_store(store, static_root, capture_date_utc, &hashes)?;

    let ta_result = write_bytes_to_static_pool(
        static_root,
        capture_date_utc,
        &ta_hash,
        &trust_anchor.ta_certificate.raw_der,
    )?;
    let mut unique = hashes.iter().cloned().collect::<BTreeSet<_>>();
    unique.insert(ta_hash.clone());
    summary.unique_hashes = unique.len();
    if ta_result.written {
        summary.written_files += 1;
    } else {
        summary.reused_files += 1;
    }
    Ok(summary)
}

pub fn export_cir_from_run(
    store: &RocksStore,
    trust_anchor: &TrustAnchor,
    tal_uri: &str,
    validation_time: time::OffsetDateTime,
    cir_out: &Path,
    static_root: &Path,
    capture_date_utc: time::Date,
) -> Result<CirExportSummary, CirExportError> {
    let cir = build_cir_from_run(store, trust_anchor, tal_uri, validation_time)?;
    let static_pool = export_cir_static_pool(store, static_root, capture_date_utc, &cir, trust_anchor)?;
    write_cir_file(cir_out, &cir)?;
    Ok(CirExportSummary {
        object_count: cir.objects.len(),
        tal_count: cir.tals.len(),
        static_pool,
    })
}

fn ta_sha256_hex(bytes: &[u8]) -> String {
    use sha2::{Digest, Sha256};
    hex::encode(Sha256::digest(bytes))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cir::decode::decode_cir;
    use crate::cir::static_pool_path;
    use crate::data_model::ta::TrustAnchor;
    use crate::data_model::tal::Tal;
    use crate::storage::{RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore};

    fn sample_time() -> time::OffsetDateTime {
        time::OffsetDateTime::parse(
            "2026-04-07T12:34:56Z",
            &time::format_description::well_known::Rfc3339,
        )
        .unwrap()
    }

    fn sample_date() -> time::Date {
        time::Date::from_calendar_date(2026, time::Month::April, 7).unwrap()
    }

    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")).unwrap();
        let ta_der = std::fs::read(base.join("tests/fixtures/ta/apnic-ta.cer")).unwrap();
        let tal = Tal::decode_bytes(&tal_bytes).unwrap();
        TrustAnchor::bind_der(tal, &ta_der, None).unwrap()
    }

    fn sha256_hex(bytes: &[u8]) -> String {
        use sha2::{Digest, Sha256};
        hex::encode(Sha256::digest(bytes))
    }

    #[test]
    fn build_cir_from_run_collects_repository_view_and_tal() {
        let td = tempfile::tempdir().unwrap();
        let store = RocksStore::open(td.path()).unwrap();
        let bytes = b"object-a".to_vec();
        let hash = sha256_hex(&bytes);
        let mut raw = RawByHashEntry::from_bytes(hash.clone(), bytes.clone());
        raw.origin_uris.push("rsync://example.test/repo/a.cer".into());
        store.put_raw_by_hash_entry(&raw).unwrap();
        store
            .put_repository_view_entry(&RepositoryViewEntry {
                rsync_uri: "rsync://example.test/repo/a.cer".to_string(),
                current_hash: Some(hash),
                repository_source: Some("https://rrdp.example.test/notification.xml".to_string()),
                object_type: Some("cer".to_string()),
                state: RepositoryViewState::Present,
            })
            .unwrap();

        let ta = sample_trust_anchor();
        let cir = build_cir_from_run(&store, &ta, "https://example.test/root.tal", sample_time())
            .expect("build cir");
        assert_eq!(cir.version, CIR_VERSION_V1);
        assert_eq!(cir.tals.len(), 1);
        assert_eq!(cir.tals[0].tal_uri, "https://example.test/root.tal");
        assert!(cir
            .objects
            .iter()
            .any(|item| item.rsync_uri == "rsync://example.test/repo/a.cer"));
        assert!(cir
            .objects
            .iter()
            .any(|item| item.rsync_uri.contains("apnic-rpki-root-iana-origin.cer")));
    }

    #[test]
    fn export_cir_from_run_writes_der_and_static_pool() {
        let td = tempfile::tempdir().unwrap();
        let store_dir = td.path().join("db");
        let out_dir = td.path().join("out");
        let static_root = td.path().join("static");
        let store = RocksStore::open(&store_dir).unwrap();

        let bytes = b"object-b".to_vec();
        let hash = sha256_hex(&bytes);
        let mut raw = RawByHashEntry::from_bytes(hash.clone(), bytes.clone());
        raw.origin_uris.push("rsync://example.test/repo/b.roa".into());
        store.put_raw_by_hash_entry(&raw).unwrap();
        store
            .put_repository_view_entry(&RepositoryViewEntry {
                rsync_uri: "rsync://example.test/repo/b.roa".to_string(),
                current_hash: Some(hash.clone()),
                repository_source: Some("https://rrdp.example.test/notification.xml".to_string()),
                object_type: Some("roa".to_string()),
                state: RepositoryViewState::Present,
            })
            .unwrap();

        let ta = sample_trust_anchor();
        let cir_path = out_dir.join("example.cir");
        let summary = export_cir_from_run(
            &store,
            &ta,
            "https://example.test/root.tal",
            sample_time(),
            &cir_path,
            &static_root,
            sample_date(),
        )
        .expect("export cir");
        assert_eq!(summary.tal_count, 1);
        assert!(summary.object_count >= 2);

        let der = std::fs::read(&cir_path).unwrap();
        let cir = decode_cir(&der).unwrap();
        assert_eq!(cir.tals[0].tal_uri, "https://example.test/root.tal");

        let object_path = static_pool_path(&static_root, sample_date(), &hash).unwrap();
        assert_eq!(std::fs::read(object_path).unwrap(), bytes);
    }
}
