use serde::Serialize;
use sha2::Digest;

use crate::policy::Policy;

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditObjectKind {
    Manifest,
    Crl,
    Certificate,
    RouterCertificate,
    Roa,
    Aspa,
    Other,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditObjectResult {
    Ok,
    Skipped,
    Error,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ObjectAuditEntry {
    pub rsync_uri: String,
    pub sha256_hex: String,
    pub kind: AuditObjectKind,
    pub result: AuditObjectResult,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditWarning {
    pub message: String,
    pub rfc_refs: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub context: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryAuditManifest {
    pub schema_version: u32,
    pub status: String,
    pub events_path: String,
    pub events_count: u64,
    pub events_sha256: String,
    pub writer_version: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidationEventCounts {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub objects: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub warnings: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vrps: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aspas: Option<u64>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidationEvent {
    pub schema_version: u32,
    pub seq: u64,
    pub event_type: String,
    pub validation_time: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pp_node_id: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pp_manifest_uri: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pp_rsync_base_uri: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repo_sync_phase: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repo_terminal_state: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub object_uri: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sha256: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub object_type: Option<AuditObjectKind>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<AuditObjectResult>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub counts: Option<ValidationEventCounts>,
}

impl From<&crate::report::Warning> for AuditWarning {
    fn from(w: &crate::report::Warning) -> Self {
        Self {
            message: w.message.clone(),
            rfc_refs: w.rfc_refs.iter().map(|r| r.0.to_string()).collect(),
            context: w.context.clone(),
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct PublicationPointAudit {
    /// Monotonic node ID assigned by the traversal engine.
    ///
    /// Present when running via the Stage2 tree engine; may be absent in ad-hoc runs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub node_id: Option<u64>,
    /// Parent node ID in the traversal tree.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_node_id: Option<u64>,
    /// Provenance metadata for non-root nodes (how this CA instance was discovered).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub discovered_from: Option<DiscoveredFrom>,

    pub rsync_base_uri: String,
    pub manifest_rsync_uri: String,
    pub publication_point_rsync_uri: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rrdp_notification_uri: Option<String>,

    pub source: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repo_sync_source: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repo_sync_phase: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repo_sync_duration_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repo_sync_error: Option<String>,
    pub repo_terminal_state: String,
    pub this_update_rfc3339_utc: String,
    pub next_update_rfc3339_utc: String,
    pub verified_at_rfc3339_utc: String,

    pub warnings: Vec<AuditWarning>,
    pub objects: Vec<ObjectAuditEntry>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct DiscoveredFrom {
    pub parent_manifest_rsync_uri: String,
    pub child_ca_certificate_rsync_uri: String,
    pub child_ca_certificate_sha256_hex: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct TreeSummary {
    pub instances_processed: usize,
    pub instances_failed: usize,
    pub warnings: Vec<AuditWarning>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditRunMeta {
    pub validation_time_rfc3339_utc: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditDownloadKind {
    RrdpNotification,
    RrdpSnapshot,
    RrdpDelta,
    Rsync,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct AuditDownloadObjectsStat {
    pub objects_count: u64,
    pub objects_bytes_total: u64,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditDownloadEvent {
    pub kind: AuditDownloadKind,
    pub uri: String,
    pub started_at_rfc3339_utc: String,
    pub finished_at_rfc3339_utc: String,
    pub duration_ms: u64,
    pub success: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bytes: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub objects: Option<AuditDownloadObjectsStat>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct AuditDownloadKindStats {
    pub ok_total: u64,
    pub fail_total: u64,
    pub duration_ms_total: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bytes_total: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub objects_count_total: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub objects_bytes_total: Option<u64>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct AuditDownloadStats {
    pub events_total: u64,
    /// Statistics keyed by serialized `AuditDownloadKind` string (e.g. "rrdp_snapshot").
    pub by_kind: std::collections::BTreeMap<String, AuditDownloadKindStats>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn publication_point_audit_serializes_object_audit_only() {
        let audit = PublicationPointAudit {
            objects: vec![ObjectAuditEntry {
                rsync_uri: "rsync://example.test/repo/fresh.roa".to_string(),
                sha256_hex: "11".repeat(32),
                kind: AuditObjectKind::Roa,
                result: AuditObjectResult::Ok,
                detail: None,
            }],
            ..PublicationPointAudit::default()
        };

        let value = serde_json::to_value(&audit).expect("serialize audit");
        assert!(value.get("objects").is_some());
        assert!(value.get("cir_fresh_objects").is_none());
        assert!(value.get("cir_cached_objects").is_none());
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct AuditRepoSyncStateStat {
    pub count: u64,
    pub duration_ms_total: u64,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct AuditRepoSyncStats {
    pub publication_points_total: u64,
    pub by_phase: std::collections::BTreeMap<String, AuditRepoSyncStateStat>,
    pub by_terminal_state: std::collections::BTreeMap<String, AuditRepoSyncStateStat>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditReportV1 {
    pub format_version: u32,
    pub meta: AuditRunMeta,
    pub policy: Policy,
    pub tree: TreeSummary,
    pub publication_points: Vec<PublicationPointAudit>,

    pub vrps: Vec<VrpOutput>,
    pub aspas: Vec<AspaOutput>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AuditReportV2 {
    pub format_version: u32,
    pub meta: AuditRunMeta,
    pub policy: Policy,
    pub tree: TreeSummary,
    pub publication_points: Vec<PublicationPointAudit>,

    pub vrps: Vec<VrpOutput>,
    pub aspas: Vec<AspaOutput>,

    pub downloads: Vec<AuditDownloadEvent>,
    pub download_stats: AuditDownloadStats,
    pub repo_sync_stats: AuditRepoSyncStats,
    #[serde(rename = "queryAudit", skip_serializing_if = "Option::is_none")]
    pub query_audit: Option<QueryAuditManifest>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct VrpOutput {
    pub asn: u32,
    pub prefix: String,
    pub max_length: u16,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AspaOutput {
    pub customer_as_id: u32,
    pub provider_as_ids: Vec<u32>,
}

pub fn sha256_hex_from_32(bytes: &[u8; 32]) -> String {
    hex::encode(bytes)
}

pub fn sha256_hex(bytes: &[u8]) -> String {
    let digest = sha2::Sha256::digest(bytes);
    hex::encode(digest)
}

pub fn format_roa_ip_prefix(p: &crate::data_model::roa::IpPrefix) -> String {
    let addr = p.addr_bytes();
    match p.afi {
        crate::data_model::roa::RoaAfi::Ipv4 => {
            format!(
                "{}.{}.{}.{}{}",
                addr[0],
                addr[1],
                addr[2],
                addr[3],
                format!("/{}", p.prefix_len)
            )
        }
        crate::data_model::roa::RoaAfi::Ipv6 => {
            let mut parts = Vec::with_capacity(8);
            for i in 0..8 {
                let hi = addr[i * 2] as u16;
                let lo = addr[i * 2 + 1] as u16;
                parts.push(format!("{:x}", (hi << 8) | lo));
            }
            format!("{}{}", parts.join(":"), format!("/{}", p.prefix_len))
        }
    }
}
