//! Versioned machine-local editor session persistence and crash boundary.

use std::path::{Path, PathBuf};

use bevy::app::AppExit;
use bevy::prelude::*;
use bevy_egui::egui;
use serde::{Deserialize, Serialize};

use crate::project_io::{ProjectWorkspace, UserPreferences};
use crate::scene::recovery::{atomic_write, default_state_root};
use crate::scene_io::{SceneIo, SceneIoRequest};
use crate::settings_ui::ProjectSettingsPanel;
use crate::ui::{BrushDiagnosticsPanel, DiagnosticsPanel};
use crate::viewport::rendering_diagnostics::RenderingDiagnosticsPanel;
use crate::viewport::CameraBookmarks;

const SESSION_FILE: &str = "session.ron";
const SESSION_SCHEMA_VERSION: u32 = 1;
const SESSION_WRITE_INTERVAL_SECS: f32 = 2.0;

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionPanelState {
    #[serde(default)]
    pub diagnostics: bool,
    #[serde(default)]
    pub brush_diagnostics: bool,
    #[serde(default)]
    pub rendering: bool,
    #[serde(default)]
    pub project_settings: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CameraBookmarkRecord {
    pub scene: String,
    pub translation: [f32; 3],
    pub rotation: [f32; 4],
    pub scale: [f32; 3],
}

impl CameraBookmarkRecord {
    fn from_transform(scene: String, transform: Transform) -> Self {
        Self {
            scene,
            translation: transform.translation.to_array(),
            rotation: transform.rotation.to_array(),
            scale: transform.scale.to_array(),
        }
    }

    fn to_transform(&self) -> Transform {
        Transform {
            translation: Vec3::from_array(self.translation),
            rotation: Quat::from_array(self.rotation).normalize(),
            scale: Vec3::from_array(self.scale),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EditorSessionDocument {
    #[serde(default)]
    pub schema_version: u32,
    #[serde(default)]
    pub clean_shutdown: bool,
    #[serde(default)]
    pub project_root: String,
    #[serde(default)]
    pub active_scene: Option<String>,
    #[serde(default)]
    pub dock_layout: Option<String>,
    #[serde(default)]
    pub hierarchy_expanded_paths: Vec<String>,
    #[serde(default)]
    pub panels: SessionPanelState,
    #[serde(default)]
    pub camera_bookmarks: Vec<CameraBookmarkRecord>,
}

impl Default for EditorSessionDocument {
    fn default() -> Self {
        Self {
            schema_version: SESSION_SCHEMA_VERSION,
            clean_shutdown: true,
            project_root: String::new(),
            active_scene: None,
            dock_layout: None,
            hierarchy_expanded_paths: Vec::new(),
            panels: SessionPanelState::default(),
            camera_bookmarks: Vec::new(),
        }
    }
}

#[derive(Resource, Debug, Default)]
pub struct EditorSessionRuntime {
    pub prior_abnormal_shutdown: bool,
    pub resume_scene: Option<PathBuf>,
    pub resume_prompt_open: bool,
    save_elapsed_secs: f32,
}

pub struct EditorSessionPlugin;

impl Plugin for EditorSessionPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<EditorSessionRuntime>()
            .add_systems(Startup, initialize_session)
            .add_systems(Update, persist_running_session)
            .add_systems(
                Last,
                persist_clean_session_on_exit.after(bevy::window::ExitSystems),
            );
    }
}

pub fn session_resume_window(world: &mut World, ctx: &egui::Context) {
    let (open, scene) = {
        let runtime = world.resource::<EditorSessionRuntime>();
        (runtime.resume_prompt_open, runtime.resume_scene.clone())
    };
    if !open {
        return;
    }
    let Some(scene) = scene else {
        world
            .resource_mut::<EditorSessionRuntime>()
            .resume_prompt_open = false;
        return;
    };

    egui::Window::new("Recover Editor Session")
        .collapsible(false)
        .resizable(false)
        .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
        .show(ctx, |ui| {
            ui.label("The previous editor session did not close cleanly.");
            ui.small("The safe startup scene is active. Resume only the last authored scene; modal tools and dirty preview state are never restored.");
            ui.monospace(scene.display().to_string());
            ui.separator();
            ui.horizontal(|ui| {
                if ui.button("Resume Last Scene").clicked() {
                    world.resource_mut::<SceneIo>().request =
                        Some(SceneIoRequest::OpenPath(scene.clone()));
                    world
                        .resource_mut::<EditorSessionRuntime>()
                        .resume_prompt_open = false;
                }
                if ui.button("Continue Safe").clicked() {
                    world
                        .resource_mut::<EditorSessionRuntime>()
                        .resume_prompt_open = false;
                    world
                        .resource_mut::<SceneIo>()
                        .set_status("Continued with safe startup after abnormal shutdown");
                }
            });
        });
}

fn session_path() -> Option<PathBuf> {
    default_state_root().map(|root| root.join(SESSION_FILE))
}

fn read_session(path: &Path) -> Result<EditorSessionDocument, String> {
    let text = std::fs::read_to_string(path)
        .map_err(|error| format!("could not read {}: {error}", path.display()))?;
    let document: EditorSessionDocument =
        ron::from_str(&text).map_err(|error| format!("invalid session document: {error}"))?;
    normalize_session(document)
}

fn normalize_session(mut document: EditorSessionDocument) -> Result<EditorSessionDocument, String> {
    match document.schema_version {
        0 => document.schema_version = SESSION_SCHEMA_VERSION,
        SESSION_SCHEMA_VERSION => {}
        version => {
            return Err(format!(
                "session schema {version} is newer than supported {SESSION_SCHEMA_VERSION}"
            ));
        }
    }
    document.camera_bookmarks.retain(valid_bookmark);
    Ok(document)
}

fn valid_bookmark(bookmark: &CameraBookmarkRecord) -> bool {
    bookmark
        .translation
        .iter()
        .chain(bookmark.rotation.iter())
        .chain(bookmark.scale.iter())
        .all(|value| value.is_finite())
}

fn write_session(path: &Path, document: &EditorSessionDocument) -> Result<(), String> {
    let text = ron::ser::to_string_pretty(document, ron::ser::PrettyConfig::default())
        .map_err(|error| format!("could not serialize session: {error}"))?;
    atomic_write(path, text.as_bytes())
}

fn initialize_session(world: &mut World) {
    let previous = session_path().and_then(|path| read_session(&path).ok());
    let prior_abnormal = previous
        .as_ref()
        .is_some_and(|document| !document.clean_shutdown);

    if let Some(document) = previous.as_ref() {
        restore_non_destructive_session_state(world, document);
        if !prior_abnormal {
            restore_active_scene(world, document);
        }
    }

    let resume_scene = previous
        .as_ref()
        .and_then(|document| document.active_scene.as_deref())
        .map(PathBuf::from)
        .filter(|path| path.exists());
    world.insert_resource(EditorSessionRuntime {
        prior_abnormal_shutdown: prior_abnormal,
        resume_prompt_open: prior_abnormal && resume_scene.is_some(),
        resume_scene,
        save_elapsed_secs: 0.0,
    });

    if let Some(path) = session_path() {
        let document = capture_session(world, false);
        if let Err(error) = write_session(&path, &document) {
            warn!("Failed to write running session marker: {error}");
        }
    }
}

fn restore_active_scene(world: &mut World, document: &EditorSessionDocument) {
    let Some(path) = document.active_scene.as_deref().map(PathBuf::from) else {
        return;
    };
    if path.exists() {
        world.resource_mut::<SceneIo>().active_path = Some(path);
    }
}

fn restore_non_destructive_session_state(world: &mut World, document: &EditorSessionDocument) {
    let poses = document
        .camera_bookmarks
        .iter()
        .map(|bookmark| (bookmark.scene.clone(), bookmark.to_transform()))
        .collect();
    world.resource_mut::<CameraBookmarks>().poses = poses;
    world.resource_mut::<DiagnosticsPanel>().open = document.panels.diagnostics;
    world.resource_mut::<BrushDiagnosticsPanel>().open = document.panels.brush_diagnostics;
    world.resource_mut::<RenderingDiagnosticsPanel>().open = document.panels.rendering;
    world.resource_mut::<ProjectSettingsPanel>().open = document.panels.project_settings;
}

fn capture_session(world: &World, clean_shutdown: bool) -> EditorSessionDocument {
    let prefs = world.resource::<UserPreferences>();
    EditorSessionDocument {
        schema_version: SESSION_SCHEMA_VERSION,
        clean_shutdown,
        project_root: world.resource::<ProjectWorkspace>().root.clone(),
        active_scene: world
            .resource::<SceneIo>()
            .active_path
            .as_ref()
            .map(|path| path.display().to_string()),
        dock_layout: prefs.dock_layout.clone(),
        hierarchy_expanded_paths: prefs.hierarchy_expanded_paths.clone(),
        panels: SessionPanelState {
            diagnostics: world.resource::<DiagnosticsPanel>().open,
            brush_diagnostics: world.resource::<BrushDiagnosticsPanel>().open,
            rendering: world.resource::<RenderingDiagnosticsPanel>().open,
            project_settings: world.resource::<ProjectSettingsPanel>().open,
        },
        camera_bookmarks: world
            .resource::<CameraBookmarks>()
            .poses
            .iter()
            .map(|(scene, transform)| {
                CameraBookmarkRecord::from_transform(scene.clone(), *transform)
            })
            .collect(),
    }
}

fn persist_running_session(world: &mut World) {
    let delta = world.resource::<Time>().delta_secs();
    let should_write = {
        let mut runtime = world.resource_mut::<EditorSessionRuntime>();
        runtime.save_elapsed_secs += delta;
        if runtime.save_elapsed_secs < SESSION_WRITE_INTERVAL_SECS {
            false
        } else {
            runtime.save_elapsed_secs = 0.0;
            true
        }
    };
    if !should_write {
        return;
    }
    let Some(path) = session_path() else {
        return;
    };
    if let Err(error) = write_session(&path, &capture_session(world, false)) {
        warn!("Failed to persist running editor session: {error}");
    }
}

#[allow(clippy::too_many_arguments)]
fn persist_clean_session_on_exit(
    mut exits: MessageReader<AppExit>,
    scene_io: Res<SceneIo>,
    workspace: Res<ProjectWorkspace>,
    prefs: Res<UserPreferences>,
    bookmarks: Res<CameraBookmarks>,
    diagnostics: Res<DiagnosticsPanel>,
    brush_diagnostics: Res<BrushDiagnosticsPanel>,
    rendering: Res<RenderingDiagnosticsPanel>,
    project_settings: Res<ProjectSettingsPanel>,
) {
    if exits.read().next().is_none() {
        return;
    }
    let Some(path) = session_path() else {
        return;
    };
    let document = EditorSessionDocument {
        schema_version: SESSION_SCHEMA_VERSION,
        clean_shutdown: true,
        project_root: workspace.root.clone(),
        active_scene: scene_io
            .active_path
            .as_ref()
            .map(|path| path.display().to_string()),
        dock_layout: prefs.dock_layout.clone(),
        hierarchy_expanded_paths: prefs.hierarchy_expanded_paths.clone(),
        panels: SessionPanelState {
            diagnostics: diagnostics.open,
            brush_diagnostics: brush_diagnostics.open,
            rendering: rendering.open,
            project_settings: project_settings.open,
        },
        camera_bookmarks: bookmarks
            .poses
            .iter()
            .map(|(scene, transform)| {
                CameraBookmarkRecord::from_transform(scene.clone(), *transform)
            })
            .collect(),
    };
    if let Err(error) = write_session(&path, &document) {
        warn!("Failed to persist clean editor shutdown: {error}");
    }
}

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

    #[test]
    fn legacy_unversioned_session_migrates_to_v1() {
        let legacy = r#"(
            clean_shutdown: true,
            project_root: "/project",
            active_scene: Some("assets/levels/test.scn.ron"),
        )"#;
        let document: EditorSessionDocument = ron::from_str(legacy).unwrap();

        let migrated = normalize_session(document).unwrap();

        assert_eq!(migrated.schema_version, SESSION_SCHEMA_VERSION);
        assert_eq!(migrated.project_root, "/project");
    }

    #[test]
    fn newer_session_schema_is_rejected() {
        let document = EditorSessionDocument {
            schema_version: SESSION_SCHEMA_VERSION + 1,
            ..default()
        };

        assert!(normalize_session(document)
            .unwrap_err()
            .contains("newer than supported"));
    }

    #[test]
    fn invalid_camera_pose_is_removed_during_migration() {
        let mut document = EditorSessionDocument::default();
        document.camera_bookmarks.push(CameraBookmarkRecord {
            scene: "scene".to_string(),
            translation: [f32::NAN, 0.0, 0.0],
            rotation: [0.0, 0.0, 0.0, 1.0],
            scale: [1.0; 3],
        });

        assert!(normalize_session(document)
            .unwrap()
            .camera_bookmarks
            .is_empty());
    }

    #[test]
    fn serialized_session_contains_only_allowlisted_metadata() {
        let document = EditorSessionDocument {
            project_root: "/project".to_string(),
            active_scene: Some("assets/levels/test.scn.ron".to_string()),
            ..default()
        };
        let text = ron::ser::to_string(&document).unwrap();

        assert!(text.contains("active_scene"));
        for forbidden in ["scene_contents", "credential", "token", "environment"] {
            assert!(!text.to_ascii_lowercase().contains(forbidden));
        }
    }
}
