//! glTF/GLB fast-path texture extraction for asset browser thumbnails.

use std::collections::HashSet;
use std::path::Path;

/// Returns an asset-server path for the first base-color texture referenced by a glTF/GLB file.
///
/// Embedded GLB buffers are skipped so callers can fall back to the render studio.
pub fn gltf_base_color_texture_path(model_asset_path: &str) -> Option<String> {
    let model_path = Path::new(model_asset_path);
    if !is_gltf_path(model_path) {
        return None;
    }

    let (document, ..) = gltf::import(model_path).ok()?;
    for material in document.materials() {
        let Some(tex_info) = material.pbr_metallic_roughness().base_color_texture() else {
            continue;
        };
        let texture = document.textures().nth(tex_info.texture().index())?;
        let image = document.images().nth(texture.source().index())?;
        let uri = match image.source() {
            gltf::image::Source::Uri { uri, .. } => uri,
            gltf::image::Source::View { .. } => continue,
        };
        if let Some(path) = resolve_texture_uri(model_path, uri) {
            return Some(path);
        }
    }
    None
}

/// Verifies local external buffers and images before Bevy starts an async glTF load.
pub fn validate_gltf_dependencies(model_asset_path: &str) -> Result<(), String> {
    let model_path = Path::new(model_asset_path);
    if !is_gltf_path(model_path) {
        return Ok(());
    }

    let gltf = gltf::Gltf::open(model_path).map_err(|error| format!("invalid glTF: {error}"))?;
    let mut uris = Vec::new();
    for buffer in gltf.buffers() {
        if let gltf::buffer::Source::Uri(uri) = buffer.source() {
            uris.push(uri);
        }
    }
    for image in gltf.images() {
        if let gltf::image::Source::Uri { uri, .. } = image.source() {
            uris.push(uri);
        }
    }

    let model_dir = model_path.parent().unwrap_or_else(|| Path::new(""));
    for uri in uris {
        if uri.starts_with("data:") || uri.contains("://") {
            continue;
        }
        let dependency = model_dir.join(uri);
        if !dependency.is_file() {
            let display = normalize_asset_path(&dependency)
                .unwrap_or_else(|| dependency.to_string_lossy().replace('\\', "/"));
            return Err(format!("missing dependency: {display}"));
        }
    }
    Ok(())
}

/// Returns primitive labels that cannot be rendered without their source skin hierarchy.
pub fn gltf_skinned_primitive_labels(model_asset_path: &str) -> HashSet<String> {
    let model_path = Path::new(model_asset_path);
    if !is_gltf_path(model_path) {
        return HashSet::new();
    }
    let Ok(gltf) = gltf::Gltf::open(model_path) else {
        return HashSet::new();
    };
    gltf.meshes()
        .flat_map(|mesh| {
            let mesh_index = mesh.index();
            mesh.primitives()
                .filter(|primitive| {
                    primitive.attributes().any(|(semantic, _)| {
                        matches!(
                            semantic,
                            gltf::Semantic::Joints(_) | gltf::Semantic::Weights(_)
                        )
                    })
                })
                .map(move |primitive| format!("Mesh{mesh_index}/Primitive{}", primitive.index()))
        })
        .collect()
}

pub struct GltfThumbnailSource;

fn is_gltf_path(path: &Path) -> bool {
    path.extension()
        .and_then(|ext| ext.to_str())
        .is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gltf" | "glb"))
}

fn resolve_texture_uri(model_path: &Path, uri: &str) -> Option<String> {
    if uri.starts_with("data:") {
        return None;
    }
    let model_dir = model_path.parent()?;
    let texture_path = model_dir.join(uri);
    normalize_asset_path(&texture_path)
}

fn normalize_asset_path(path: &Path) -> Option<String> {
    let raw = path.to_string_lossy().replace('\\', "/");
    if raw.starts_with("assets/") {
        return Some(raw);
    }
    let file_name = path.file_name()?.to_str()?;
    if file_name.is_empty() {
        return None;
    }
    Some(format!("assets/{file_name}"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::path::PathBuf;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn normalize_asset_path_keeps_assets_prefix() {
        assert_eq!(
            normalize_asset_path(Path::new("assets/textures/albedo.png")),
            Some("assets/textures/albedo.png".into())
        );
    }

    #[test]
    fn resolve_texture_uri_relative_to_model() {
        let model = PathBuf::from("assets/models/chair.gltf");
        assert_eq!(
            resolve_texture_uri(&model, "textures/chair_albedo.png"),
            Some("assets/models/textures/chair_albedo.png".into())
        );
    }

    #[test]
    fn dependency_validation_reports_missing_external_buffer() {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let directory = std::env::temp_dir().join(format!("blacksite-gltf-{nonce}"));
        fs::create_dir_all(&directory).unwrap();
        let model = directory.join("missing-buffer.gltf");
        fs::write(
            &model,
            r#"{"asset":{"version":"2.0"},"buffers":[{"uri":"missing.bin","byteLength":4}]}"#,
        )
        .unwrap();

        let error = validate_gltf_dependencies(model.to_str().unwrap()).unwrap_err();
        assert!(error.contains("missing.bin"));

        fs::remove_dir_all(directory).unwrap();
    }

    #[test]
    fn committed_robot_fixture_identifies_skinned_primitives() {
        let path =
            Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/models/robot_expressive.glb");
        let labels = gltf_skinned_primitive_labels(path.to_str().unwrap());

        assert!(labels.contains("Mesh12/Primitive0"));
        assert!(labels.contains("Mesh13/Primitive0"));
        assert!(!labels.contains("Mesh0/Primitive0"));
        assert!(!labels.contains("Mesh999/Primitive0"));
    }
}
