use std::collections::HashMap;

use crate::audit::{AuditObjectResult, ObjectAuditEntry};
use crate::cir::export::CirExportError;
use crate::cir::model::{CirObject, CirRejectedObject};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CirInputSection {
    Fresh,
    Cached,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CirInputSnapshot {
    pub fresh_validated_objects: Vec<CirObject>,
    pub cached_validated_objects: Vec<CirObject>,
    pub fresh_rejected_objects: Vec<CirRejectedObject>,
    pub cached_rejected_objects: Vec<CirRejectedObject>,
}

#[derive(Clone, Debug, Default)]
pub struct CirInputAccumulator {
    fresh_objects: HashMap<String, [u8; 32]>,
    cached_objects: HashMap<String, [u8; 32]>,
    fresh_rejects: HashMap<String, Option<String>>,
    cached_rejects: HashMap<String, Option<String>>,
}

impl CirInputAccumulator {
    pub fn submit_audit_entries(
        &mut self,
        section: CirInputSection,
        entries: &[ObjectAuditEntry],
    ) -> Result<(), CirExportError> {
        for entry in entries {
            if !matches!(
                entry.result,
                AuditObjectResult::Ok | AuditObjectResult::Error
            ) {
                continue;
            }
            self.insert_object(section, &entry.rsync_uri, &entry.sha256_hex)?;
            if entry.result == AuditObjectResult::Error {
                self.insert_reject(section, &entry.rsync_uri, entry.detail.clone());
            }
        }
        Ok(())
    }

    pub fn submit_snapshot(&mut self, snapshot: CirInputSnapshot) -> Result<(), CirExportError> {
        for object in snapshot.fresh_validated_objects {
            self.insert_object_bytes(CirInputSection::Fresh, &object.rsync_uri, object.sha256)?;
        }
        for object in snapshot.cached_validated_objects {
            self.insert_object_bytes(CirInputSection::Cached, &object.rsync_uri, object.sha256)?;
        }
        for rejected in snapshot.fresh_rejected_objects {
            self.insert_reject(
                CirInputSection::Fresh,
                &rejected.object_uri,
                rejected.reason,
            );
        }
        for rejected in snapshot.cached_rejected_objects {
            self.insert_reject(
                CirInputSection::Cached,
                &rejected.object_uri,
                rejected.reason,
            );
        }
        Ok(())
    }

    pub fn insert_object(
        &mut self,
        section: CirInputSection,
        rsync_uri: &str,
        sha256_hex: &str,
    ) -> Result<(), CirExportError> {
        if !rsync_uri.starts_with("rsync://") || !is_sha256_hex(sha256_hex) {
            return Ok(());
        }
        let mut digest = [0u8; 32];
        hex::decode_to_slice(sha256_hex, &mut digest).expect("validated sha256 hex");
        self.insert_object_digest(section, rsync_uri, digest)
    }

    fn insert_object_bytes(
        &mut self,
        section: CirInputSection,
        rsync_uri: &str,
        sha256: Vec<u8>,
    ) -> Result<(), CirExportError> {
        if !rsync_uri.starts_with("rsync://") {
            return Ok(());
        }
        let Ok(digest) = <[u8; 32]>::try_from(sha256) else {
            return Ok(());
        };
        self.insert_object_digest(section, rsync_uri, digest)
    }

    fn insert_object_digest(
        &mut self,
        section: CirInputSection,
        rsync_uri: &str,
        digest: [u8; 32],
    ) -> Result<(), CirExportError> {
        let objects = match section {
            CirInputSection::Fresh => &mut self.fresh_objects,
            CirInputSection::Cached => &mut self.cached_objects,
        };
        if let Some(existing) = objects.get(rsync_uri) {
            if existing != &digest {
                return Err(CirExportError::ConflictingObjectHash {
                    rsync_uri: rsync_uri.to_string(),
                    first: hex::encode(existing),
                    second: hex::encode(digest),
                });
            }
            return Ok(());
        }
        objects.insert(rsync_uri.to_string(), digest);
        Ok(())
    }

    pub fn insert_reject(
        &mut self,
        section: CirInputSection,
        rsync_uri: &str,
        reason: Option<String>,
    ) {
        if !rsync_uri.starts_with("rsync://") {
            return;
        }
        let rejects = match section {
            CirInputSection::Fresh => &mut self.fresh_rejects,
            CirInputSection::Cached => &mut self.cached_rejects,
        };
        rejects.entry(rsync_uri.to_string()).or_insert(reason);
    }

    /// Merge another accumulator into this one. Object digests conflict-check
    /// exactly like repeated `insert_object` calls; rejects keep the first
    /// reason seen, matching sequential submission order when `other` holds
    /// later publication points than `self`.
    pub fn merge(&mut self, other: CirInputAccumulator) -> Result<(), CirExportError> {
        for (rsync_uri, digest) in other.fresh_objects {
            self.insert_object_digest(CirInputSection::Fresh, &rsync_uri, digest)?;
        }
        for (rsync_uri, digest) in other.cached_objects {
            self.insert_object_digest(CirInputSection::Cached, &rsync_uri, digest)?;
        }
        for (rsync_uri, reason) in other.fresh_rejects {
            self.insert_reject(CirInputSection::Fresh, &rsync_uri, reason);
        }
        for (rsync_uri, reason) in other.cached_rejects {
            self.insert_reject(CirInputSection::Cached, &rsync_uri, reason);
        }
        Ok(())
    }

    pub fn finalize(self) -> CirInputSnapshot {
        CirInputSnapshot {
            fresh_validated_objects: finalize_objects(self.fresh_objects),
            cached_validated_objects: finalize_objects(self.cached_objects),
            fresh_rejected_objects: finalize_rejects(self.fresh_rejects),
            cached_rejected_objects: finalize_rejects(self.cached_rejects),
        }
    }
}

fn finalize_objects(objects: HashMap<String, [u8; 32]>) -> Vec<CirObject> {
    let mut objects = objects
        .into_iter()
        .map(|(rsync_uri, sha256)| CirObject {
            rsync_uri,
            sha256: sha256.to_vec(),
        })
        .collect::<Vec<_>>();
    objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
    objects
}

fn finalize_rejects(rejects: HashMap<String, Option<String>>) -> Vec<CirRejectedObject> {
    let mut rejects = rejects
        .into_iter()
        .map(|(object_uri, reason)| CirRejectedObject { object_uri, reason })
        .collect::<Vec<_>>();
    rejects.sort_by(|a, b| a.object_uri.cmp(&b.object_uri));
    rejects
}

fn is_sha256_hex(value: &str) -> bool {
    value.len() == 64 && value.as_bytes().iter().all(u8::is_ascii_hexdigit)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::audit::{AuditObjectKind, AuditObjectResult};

    fn entry(uri: &str, hash_byte: u8, result: AuditObjectResult) -> ObjectAuditEntry {
        let is_error = result == AuditObjectResult::Error;
        ObjectAuditEntry {
            rsync_uri: uri.to_string(),
            sha256_hex: format!("{}", hex::encode([hash_byte; 32])),
            kind: AuditObjectKind::Roa,
            result,
            detail: is_error.then(|| "invalid".to_string()),
        }
    }

    #[test]
    fn accumulator_finalizes_sorted_fresh_and_cached_sections() {
        let mut acc = CirInputAccumulator::default();
        acc.submit_audit_entries(
            CirInputSection::Fresh,
            &[
                entry("rsync://example.net/z.roa", 0x22, AuditObjectResult::Ok),
                entry("rsync://example.net/a.roa", 0x11, AuditObjectResult::Error),
            ],
        )
        .unwrap();
        acc.submit_audit_entries(
            CirInputSection::Cached,
            &[entry(
                "rsync://example.net/c.roa",
                0x33,
                AuditObjectResult::Ok,
            )],
        )
        .unwrap();
        let snapshot = acc.finalize();
        assert_eq!(
            snapshot.fresh_validated_objects[0].rsync_uri,
            "rsync://example.net/a.roa"
        );
        assert_eq!(
            snapshot.fresh_validated_objects[1].rsync_uri,
            "rsync://example.net/z.roa"
        );
        assert_eq!(
            snapshot.cached_validated_objects[0].rsync_uri,
            "rsync://example.net/c.roa"
        );
        assert_eq!(
            snapshot.fresh_rejected_objects[0].object_uri,
            "rsync://example.net/a.roa"
        );
        assert!(snapshot.cached_rejected_objects.is_empty());
    }

    #[test]
    fn accumulator_rejects_conflicting_hashes_within_same_section() {
        let mut acc = CirInputAccumulator::default();
        acc.submit_audit_entries(
            CirInputSection::Fresh,
            &[entry(
                "rsync://example.net/a.roa",
                0x11,
                AuditObjectResult::Ok,
            )],
        )
        .unwrap();
        let err = acc
            .submit_audit_entries(
                CirInputSection::Fresh,
                &[entry(
                    "rsync://example.net/a.roa",
                    0x22,
                    AuditObjectResult::Ok,
                )],
            )
            .unwrap_err();
        assert!(matches!(err, CirExportError::ConflictingObjectHash { .. }));
    }

    #[test]
    fn accumulator_merges_finalized_snapshots() {
        let snapshot = CirInputSnapshot {
            fresh_validated_objects: vec![CirObject {
                rsync_uri: "rsync://example.net/fresh.roa".to_string(),
                sha256: vec![0x11; 32],
            }],
            cached_validated_objects: vec![CirObject {
                rsync_uri: "rsync://example.net/cached.roa".to_string(),
                sha256: vec![0x22; 32],
            }],
            fresh_rejected_objects: vec![CirRejectedObject {
                object_uri: "rsync://example.net/fresh.roa".to_string(),
                reason: Some("fresh invalid".to_string()),
            }],
            cached_rejected_objects: Vec::new(),
        };
        let mut acc = CirInputAccumulator::default();
        acc.submit_snapshot(snapshot).unwrap();
        let merged = acc.finalize();
        assert_eq!(merged.fresh_validated_objects.len(), 1);
        assert_eq!(merged.cached_validated_objects.len(), 1);
        assert_eq!(merged.fresh_rejected_objects.len(), 1);
    }

    #[test]
    fn accumulator_merge_matches_sequential_submission() {
        let mut sequential = CirInputAccumulator::default();
        sequential
            .submit_audit_entries(
                CirInputSection::Fresh,
                &[
                    entry("rsync://example.net/a.roa", 0x11, AuditObjectResult::Ok),
                    entry("rsync://example.net/b.roa", 0x22, AuditObjectResult::Error),
                ],
            )
            .unwrap();
        sequential
            .submit_audit_entries(
                CirInputSection::Cached,
                &[entry(
                    "rsync://example.net/c.roa",
                    0x33,
                    AuditObjectResult::Ok,
                )],
            )
            .unwrap();

        let mut first = CirInputAccumulator::default();
        first
            .submit_audit_entries(
                CirInputSection::Fresh,
                &[entry("rsync://example.net/a.roa", 0x11, AuditObjectResult::Ok)],
            )
            .unwrap();
        let mut second = CirInputAccumulator::default();
        second
            .submit_audit_entries(
                CirInputSection::Fresh,
                &[entry(
                    "rsync://example.net/b.roa",
                    0x22,
                    AuditObjectResult::Error,
                )],
            )
            .unwrap();
        second
            .submit_audit_entries(
                CirInputSection::Cached,
                &[entry(
                    "rsync://example.net/c.roa",
                    0x33,
                    AuditObjectResult::Ok,
                )],
            )
            .unwrap();
        first.merge(second).unwrap();
        assert_eq!(sequential.finalize(), first.finalize());
    }

    #[test]
    fn accumulator_merge_detects_conflicting_hashes_across_shards() {
        let mut first = CirInputAccumulator::default();
        first
            .insert_object(CirInputSection::Fresh, "rsync://example.net/a.roa", &"11".repeat(32))
            .unwrap();
        let mut second = CirInputAccumulator::default();
        second
            .insert_object(CirInputSection::Fresh, "rsync://example.net/a.roa", &"22".repeat(32))
            .unwrap();
        let err = first.merge(second).unwrap_err();
        assert!(matches!(err, CirExportError::ConflictingObjectHash { .. }));
    }

    #[test]
    fn accumulator_merge_keeps_first_reject_reason() {
        let mut first = CirInputAccumulator::default();
        first.insert_reject(
            CirInputSection::Fresh,
            "rsync://example.net/a.roa",
            Some("first".to_string()),
        );
        let mut second = CirInputAccumulator::default();
        second.insert_reject(
            CirInputSection::Fresh,
            "rsync://example.net/a.roa",
            Some("second".to_string()),
        );
        first.merge(second).unwrap();
        let snapshot = first.finalize();
        assert_eq!(
            snapshot.fresh_rejected_objects[0].reason,
            Some("first".to_string())
        );
    }
}
