//! Structured diagnostic bundle and the redaction pass over it.
//!
//! Mirrors `docs/security/diagnostic-redaction-audit-report.md` §5
//! "Export Bundle Contents". Fields default to safe values; sensitive
//! categories are excluded by default.

use serde::{Deserialize, Serialize};

use crate::redactor::Redactor;
use crate::REDACTION_MARKER;

/// Raw bundle assembled by the application before redaction. The
/// caller MUST pass this through `Redactor::redact_bundle` before any
/// disk write or user-visible export.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DiagnosticBundle {
    pub app_version: String,
    pub build_number: String,
    pub platform_info: String,
    pub connection_state: String,
    pub server_address: Option<String>,
    pub channel_tree: Vec<String>,
    pub chat_history: Vec<String>,
    pub log_lines: Vec<String>,
    pub audio_device_names: Vec<String>,
    /// Free-form extras keyed by field name. Sensitive field names
    /// (matched against `RedactionPolicy::redact_field_substrings`)
    /// are redacted wholesale; everything else flows through
    /// `redact_text` for value-level scrubbing.
    pub extras: std::collections::BTreeMap<String, String>,
}

/// Final, redacted bundle suitable for export. Produced by
/// `Redactor::redact_bundle`. The shape is intentionally the same
/// type so callers can re-serialize as JSON without conversion.
pub type RedactedBundle = DiagnosticBundle;

impl Redactor {
    /// Apply the redaction policy to `bundle`, returning a copy safe
    /// for export.
    pub fn redact_bundle(&self, bundle: &DiagnosticBundle) -> RedactedBundle {
        let policy = self.policy();

        let chat_history = if policy.include_chat {
            bundle
                .chat_history
                .iter()
                .map(|m| self.redact_text(m))
                .collect()
        } else {
            // Chat is excluded by default (audit report §5 / REDACT-TC-004).
            Vec::new()
        };

        let channel_tree = if policy.include_channel_tree {
            bundle
                .channel_tree
                .iter()
                .map(|c| self.redact_text(c))
                .collect()
        } else {
            Vec::new()
        };

        let mut redacted_extras = std::collections::BTreeMap::new();
        for (k, v) in &bundle.extras {
            if self.is_sensitive_field(k) {
                redacted_extras.insert(k.clone(), REDACTION_MARKER.to_string());
            } else {
                redacted_extras.insert(k.clone(), self.redact_text(v));
            }
        }

        RedactedBundle {
            app_version: bundle.app_version.clone(),
            build_number: bundle.build_number.clone(),
            // Platform info gets a soft scrub (paths, usernames).
            platform_info: self.redact_text(&bundle.platform_info),
            connection_state: self.redact_text(&bundle.connection_state),
            server_address: bundle.server_address.as_ref().map(|s| self.redact_text(s)),
            channel_tree,
            chat_history,
            log_lines: bundle
                .log_lines
                .iter()
                .map(|l| self.redact_text(l))
                .collect(),
            audio_device_names: bundle
                .audio_device_names
                .iter()
                .map(|d| self.redact_text(d))
                .collect(),
            extras: redacted_extras,
        }
    }
}
