//! Cached project sample catalog used by editor scene-opening UI.

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

use bevy::prelude::*;
use scene::sample_pack::{
    validate_editor_sample_pack, EditorSampleEntry, EDITOR_SAMPLE_MANIFEST_PATH,
};

use super::project_io::ProjectWorkspace;

const VALIDATION_HINT: &str = "Run `cargo validate-samples --project .` for details.";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SampleCatalogEntry {
    pub id: String,
    pub label: String,
    pub scene_path: PathBuf,
    pub project_relative_scene: String,
    pub summary: String,
    pub checks: Vec<String>,
}

impl SampleCatalogEntry {
    pub fn hover_text(&self) -> String {
        let mut lines = vec![self.summary.clone()];
        if !self.checks.is_empty() {
            lines.push(format!("Checks: {}", self.checks.join(", ")));
        }
        lines.push(self.project_relative_scene.clone());
        lines.join("\n")
    }
}

#[derive(Resource, Debug, Clone)]
pub struct SampleCatalog {
    entries: Vec<SampleCatalogEntry>,
    unavailable_reason: Option<String>,
}

impl Default for SampleCatalog {
    fn default() -> Self {
        Self::unavailable("Sample catalog is loading")
    }
}

impl SampleCatalog {
    pub fn entries(&self) -> &[SampleCatalogEntry] {
        &self.entries
    }

    pub fn entry(&self, stable_id: &str) -> Option<&SampleCatalogEntry> {
        self.unavailable_reason
            .is_none()
            .then(|| self.entries.iter().find(|entry| entry.id == stable_id))
            .flatten()
    }

    pub fn unavailable_reason(&self) -> Option<&str> {
        self.unavailable_reason.as_deref()
    }

    #[cfg(test)]
    pub(crate) fn available_entries(entries: Vec<SampleCatalogEntry>) -> Self {
        Self {
            entries,
            unavailable_reason: None,
        }
    }

    fn available(samples: Vec<EditorSampleEntry>) -> Self {
        Self {
            entries: samples.into_iter().map(catalog_entry).collect(),
            unavailable_reason: None,
        }
    }

    fn unavailable(reason: impl Into<String>) -> Self {
        Self {
            entries: Vec::new(),
            unavailable_reason: Some(reason.into()),
        }
    }
}

fn catalog_entry(entry: EditorSampleEntry) -> SampleCatalogEntry {
    SampleCatalogEntry {
        id: entry.id,
        label: entry.label,
        scene_path: PathBuf::from(&entry.scene),
        project_relative_scene: entry.scene,
        summary: entry.summary,
        checks: entry.checks,
    }
}

fn load_catalog(project_root: &Path) -> SampleCatalog {
    let validation = validate_editor_sample_pack(project_root);
    if !validation.is_release_ready() {
        let first_error = validation
            .report
            .findings
            .iter()
            .find(|finding| finding.severity == scene::ValidationSeverity::Error)
            .map(|finding| finding.message.as_str())
            .unwrap_or("sample pack validation failed");
        return SampleCatalog::unavailable(format!(
            "Sample manifest `{EDITOR_SAMPLE_MANIFEST_PATH}` is invalid: {first_error}\n{VALIDATION_HINT}"
        ));
    }
    if validation.samples.is_empty() {
        return SampleCatalog::unavailable(format!(
            "Sample manifest contains no scenes.\n{VALIDATION_HINT}"
        ));
    }
    SampleCatalog::available(validation.samples)
}

fn load_sample_catalog(world: &mut World) {
    let project_root = PathBuf::from(&world.resource::<ProjectWorkspace>().root);
    world.insert_resource(load_catalog(&project_root));
}

pub struct SampleCatalogPlugin;

impl Plugin for SampleCatalogPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<SampleCatalog>()
            .add_systems(Startup, load_sample_catalog);
    }
}

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

    fn sample(id: &str, label: &str, scene: &str) -> EditorSampleEntry {
        EditorSampleEntry {
            id: id.into(),
            label: label.into(),
            area: EditorSampleArea::Brush,
            scene: scene.into(),
            summary: format!("Summary for {label}"),
            checks: vec!["selection".into(), "undo".into()],
        }
    }

    #[test]
    fn available_catalog_preserves_manifest_order_and_resolves_paths() {
        let catalog = SampleCatalog::available(vec![
            sample(
                "terrain",
                "Terrain",
                "assets/levels/samples/terrain.scn.ron",
            ),
            sample("brush", "Brush", "assets/levels/samples/brush.scn.ron"),
        ]);

        assert_eq!(
            catalog
                .entries()
                .iter()
                .map(|entry| entry.id.as_str())
                .collect::<Vec<_>>(),
            vec!["terrain", "brush"]
        );
        assert_eq!(
            catalog.entry("brush").unwrap().scene_path,
            PathBuf::from("assets/levels/samples/brush.scn.ron")
        );
        let hover = catalog.entry("brush").unwrap().hover_text();
        assert!(hover.contains("Summary for Brush"));
        assert!(hover.contains("Checks: selection, undo"));
        assert!(hover.contains("assets/levels/samples/brush.scn.ron"));
    }

    #[test]
    fn unavailable_catalog_never_resolves_entries() {
        let catalog = SampleCatalog {
            entries: vec![catalog_entry(sample(
                "brush",
                "Brush",
                "assets/levels/samples/brush.scn.ron",
            ))],
            unavailable_reason: Some("manifest invalid".into()),
        };

        assert!(catalog.entry("brush").is_none());
        assert_eq!(catalog.unavailable_reason(), Some("manifest invalid"));
    }

    #[test]
    fn missing_manifest_reports_repair_command() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-missing-sample-catalog-{}",
            uuid::Uuid::new_v4()
        ));
        std::fs::create_dir_all(&root).unwrap();

        let catalog = load_catalog(&root);

        let reason = catalog.unavailable_reason().unwrap();
        assert!(reason.contains(EDITOR_SAMPLE_MANIFEST_PATH));
        assert!(reason.contains("cargo validate-samples --project ."));
        std::fs::remove_dir_all(root).unwrap();
    }
}
