//! Privacy-bounded support bundle export for editor troubleshooting.

use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use bevy::prelude::*;
use bevy::render::renderer::RenderAdapterInfo;
use serde::{Deserialize, Serialize};
use shared::{brush_math::validate_brush, BrushDesc, LevelObject};

use crate::project_io::ProjectWorkspace;
use crate::scene::recovery::{atomic_write, default_state_root};
use crate::scene_io::{SceneIo, SceneIoEventSeverity};
use crate::session::EditorSessionRuntime;

const DIAGNOSTICS_SCHEMA_VERSION: u32 = 1;
const DIAGNOSTICS_DIR: &str = "diagnostics";
const MAX_EVENT_MESSAGE_CHARS: usize = 512;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiagnosticBuildInfo {
    pub editor_version: String,
    pub bevy_version: String,
    pub target_os: String,
    pub target_arch: String,
    pub build_profile: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiagnosticRendererInfo {
    pub adapter_name: String,
    pub device_type: String,
    pub backend: String,
    pub driver: String,
    pub driver_info: String,
    pub vendor_id: u32,
    pub device_id: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiagnosticProjectInfo {
    pub project_root: String,
    pub settings_path: String,
    pub active_scene: Option<String>,
    pub scene_dirty: bool,
    pub project_settings_dirty: bool,
    pub layout_dirty: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiagnosticValidationSummary {
    pub level_object_count: usize,
    pub brush_actor_count: usize,
    pub invalid_brush_count: usize,
    pub brush_error_count: usize,
    pub brush_warning_count: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiagnosticSceneIoEvent {
    pub id: u64,
    pub severity: String,
    pub message: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiagnosticBundleDocument {
    pub schema_version: u32,
    pub created_unix_seconds: u64,
    pub build: DiagnosticBuildInfo,
    pub renderer: Option<DiagnosticRendererInfo>,
    pub project: DiagnosticProjectInfo,
    pub validation: DiagnosticValidationSummary,
    pub prior_abnormal_shutdown: bool,
    pub scene_io_events: Vec<DiagnosticSceneIoEvent>,
}

/// Writes an allowlisted support report without scene data, environment values, or credentials.
pub fn export_diagnostic_bundle(world: &mut World) -> Result<PathBuf, String> {
    let document = capture_diagnostic_bundle(world);
    let directory = default_state_root()
        .ok_or_else(|| "XDG_STATE_HOME and HOME are both unavailable".to_string())?
        .join(DIAGNOSTICS_DIR);
    let timestamp = document.created_unix_seconds;
    let path = directory.join(format!(
        "blacksite-diagnostics-{timestamp:020}-{}.ron",
        std::process::id()
    ));
    let text = serialize_diagnostic_bundle(&document)?;
    atomic_write(&path, text.as_bytes())?;
    Ok(path)
}

fn capture_diagnostic_bundle(world: &mut World) -> DiagnosticBundleDocument {
    let validation = validation_summary(world);
    let renderer =
        world
            .get_resource::<RenderAdapterInfo>()
            .map(|adapter| DiagnosticRendererInfo {
                adapter_name: adapter.name.clone(),
                device_type: format!("{:?}", adapter.device_type),
                backend: format!("{:?}", adapter.backend),
                driver: adapter.driver.clone(),
                driver_info: adapter.driver_info.clone(),
                vendor_id: adapter.vendor,
                device_id: adapter.device,
            });
    let scene_io = world.resource::<SceneIo>();
    let workspace = world.resource::<ProjectWorkspace>();
    let project = DiagnosticProjectInfo {
        project_root: workspace.root.clone(),
        settings_path: workspace.settings_path.clone(),
        active_scene: scene_io
            .active_path
            .as_ref()
            .map(|path| path.display().to_string()),
        scene_dirty: scene_io.has_unsaved_tabs(),
        project_settings_dirty: workspace.settings_dirty,
        layout_dirty: workspace.layout_dirty,
    };
    let scene_io_events = scene_io
        .events
        .iter()
        .map(|event| DiagnosticSceneIoEvent {
            id: event.id,
            severity: match event.severity {
                SceneIoEventSeverity::Info => "info",
                SceneIoEventSeverity::Error => "error",
            }
            .to_string(),
            message: bounded_message(&event.message),
        })
        .collect();

    DiagnosticBundleDocument {
        schema_version: DIAGNOSTICS_SCHEMA_VERSION,
        created_unix_seconds: SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs(),
        build: build_info(),
        renderer,
        project,
        validation,
        prior_abnormal_shutdown: world
            .get_resource::<EditorSessionRuntime>()
            .is_some_and(|runtime| runtime.prior_abnormal_shutdown),
        scene_io_events,
    }
}

fn validation_summary(world: &mut World) -> DiagnosticValidationSummary {
    let mut level_query = world.query_filtered::<Entity, With<LevelObject>>();
    let level_object_count = level_query.iter(world).count();
    let mut brush_query = world.query_filtered::<&BrushDesc, With<LevelObject>>();
    let mut summary = DiagnosticValidationSummary {
        level_object_count,
        brush_actor_count: 0,
        invalid_brush_count: 0,
        brush_error_count: 0,
        brush_warning_count: 0,
    };
    for brush in brush_query.iter(world) {
        summary.brush_actor_count += 1;
        let report = validate_brush(brush);
        let errors = report
            .diagnostics
            .iter()
            .filter(|diagnostic| {
                diagnostic.severity == shared::brush_math::BrushDiagnosticSeverity::Error
            })
            .count();
        let warnings = report.diagnostics.len().saturating_sub(errors);
        summary.brush_error_count += errors;
        summary.brush_warning_count += warnings;
        summary.invalid_brush_count += usize::from(errors > 0);
    }
    summary
}

fn build_info() -> DiagnosticBuildInfo {
    DiagnosticBuildInfo {
        editor_version: env!("CARGO_PKG_VERSION").to_string(),
        bevy_version: "0.19".to_string(),
        target_os: std::env::consts::OS.to_string(),
        target_arch: std::env::consts::ARCH.to_string(),
        build_profile: if cfg!(debug_assertions) {
            "debug"
        } else {
            "release"
        }
        .to_string(),
    }
}

fn bounded_message(message: &str) -> String {
    message
        .chars()
        .filter(|character| !character.is_control() || *character == ' ')
        .take(MAX_EVENT_MESSAGE_CHARS)
        .collect()
}

fn serialize_diagnostic_bundle(document: &DiagnosticBundleDocument) -> Result<String, String> {
    ron::ser::to_string_pretty(document, ron::ser::PrettyConfig::default())
        .map_err(|error| format!("could not serialize diagnostics: {error}"))
}

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

    fn fixture() -> DiagnosticBundleDocument {
        DiagnosticBundleDocument {
            schema_version: DIAGNOSTICS_SCHEMA_VERSION,
            created_unix_seconds: 42,
            build: build_info(),
            renderer: None,
            project: DiagnosticProjectInfo {
                project_root: "/project".to_string(),
                settings_path: "assets/project.ron".to_string(),
                active_scene: Some("assets/levels/test.scn.ron".to_string()),
                scene_dirty: true,
                project_settings_dirty: false,
                layout_dirty: false,
            },
            validation: DiagnosticValidationSummary {
                level_object_count: 12,
                brush_actor_count: 2,
                invalid_brush_count: 1,
                brush_error_count: 1,
                brush_warning_count: 0,
            },
            prior_abnormal_shutdown: true,
            scene_io_events: vec![DiagnosticSceneIoEvent {
                id: 1,
                severity: "error".to_string(),
                message: "Save failed: disk full".to_string(),
            }],
        }
    }

    #[test]
    fn serialized_bundle_contains_only_allowlisted_sections() {
        let text = serialize_diagnostic_bundle(&fixture()).unwrap();

        for allowed in [
            "build",
            "renderer",
            "project",
            "validation",
            "scene_io_events",
        ] {
            assert!(text.contains(allowed));
        }
        for forbidden in [
            "scene_contents",
            "asset_contents",
            "environment",
            "credential",
            "access_token",
            "hostname",
            "username",
            "modal_operator",
        ] {
            assert!(!text.to_ascii_lowercase().contains(forbidden));
        }
    }

    #[test]
    fn event_messages_are_control_free_and_bounded() {
        let message = format!("failure\n{}", "x".repeat(MAX_EVENT_MESSAGE_CHARS + 20));
        let bounded = bounded_message(&message);

        assert!(bounded.chars().count() <= MAX_EVENT_MESSAGE_CHARS);
        assert!(!bounded.contains('\n'));
    }
}
