//! Project asset registry (stable IDs + import metadata). Phase 5 foundation.

use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use uuid::Uuid;

/// Stable asset identity for dependency tracking and prefab references.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct AssetId(pub Uuid);

impl AssetId {
    pub fn new() -> Self {
        Self(Uuid::new_v4())
    }

    pub fn as_string(&self) -> String {
        self.0.to_string()
    }
}

impl Default for AssetId {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum ModelPlacementMode {
    /// Place renderable content through the appropriate normalized renderer. Imports containing
    /// skins or animation route to `SkinnedMeshRenderer`; unrigged, non-animated content routes to
    /// `StaticMeshRenderer`.
    #[default]
    StaticAsset,
    /// Instantiate the complete source scene as a generic imported model.
    SceneInstance,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum ModelHierarchyMode {
    #[default]
    SingleActor,
    SourceHierarchy,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum MaterialImportPolicy {
    #[default]
    SourceMaterials,
    AuthoringOverride,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImportSettings {
    pub scale: f32,
    pub generate_collider: bool,
    pub lod0_only: bool,
    #[serde(default)]
    pub placement_mode: ModelPlacementMode,
    #[serde(default)]
    pub hierarchy_mode: ModelHierarchyMode,
    #[serde(default)]
    pub material_policy: MaterialImportPolicy,
    #[serde(default)]
    pub static_mesh_manifest_path: Option<String>,
    #[serde(default)]
    pub animation_manifest_path: Option<String>,
    /// Stable animation clip sub-asset ID used as this model's edit-mode rest presentation.
    /// `None` preserves the imported node pose and never guesses a clip.
    #[serde(default)]
    pub default_animation_clip_id: Option<String>,
}

impl Default for ImportSettings {
    fn default() -> Self {
        Self {
            scale: 1.0,
            generate_collider: true,
            lod0_only: true,
            placement_mode: ModelPlacementMode::default(),
            hierarchy_mode: ModelHierarchyMode::default(),
            material_policy: MaterialImportPolicy::default(),
            static_mesh_manifest_path: None,
            animation_manifest_path: None,
            default_animation_clip_id: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AssetRecord {
    pub id: AssetId,
    pub path: String,
    pub label: String,
    pub kind_tag: String,
    #[serde(default)]
    pub import_settings: ImportSettings,
    #[serde(default)]
    pub dependencies: Vec<String>,
}

#[derive(Resource, Debug, Default)]
pub struct AssetRegistry {
    pub records: Vec<AssetRecord>,
    pub index_dirty: bool,
}

pub struct AssetDbPlugin;

impl Plugin for AssetDbPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(load_registry())
            .add_systems(Update, sync_registry_from_browser);
    }
}

fn sync_registry_from_browser(
    assets: Res<crate::assets::EditorAssets>,
    mut registry: ResMut<AssetRegistry>,
) {
    if !assets.is_changed() {
        return;
    }

    let mut existing: HashMap<String, AssetRecord> = registry
        .records
        .drain(..)
        .map(|record| (record.path.clone(), record))
        .collect();
    let current_paths = assets
        .assets
        .iter()
        .filter_map(|asset| asset.path.clone())
        .collect::<HashSet<_>>();

    let mut next_records = Vec::new();
    let mut seen_paths = HashMap::new();

    for asset in &assets.assets {
        let Some(path) = asset.path.clone() else {
            continue;
        };
        if path.is_empty() || seen_paths.contains_key(&path) {
            continue;
        }
        seen_paths.insert(path.clone(), ());

        let kind_tag = format!("{:?}", asset.kind);
        let mut record = if let Some(mut prior) = existing.remove(&path).or_else(|| {
            take_uniquely_moved_model_record(&mut existing, &current_paths, &path, &kind_tag)
        }) {
            if prior.path != path {
                info!(
                    "Asset registry preserved model identity {} across move {} -> {}",
                    prior.id.as_string(),
                    prior.path,
                    path
                );
                prior.path = path.clone();
            }
            prior.label = asset.label.clone();
            prior.kind_tag = kind_tag;
            prior
        } else {
            AssetRecord {
                id: AssetId::new(),
                path: path.clone(),
                label: asset.label.clone(),
                kind_tag,
                import_settings: ImportSettings::default(),
                dependencies: Vec::new(),
            }
        };
        if record.kind_tag == "Model" {
            if let Err(error) = super::refresh_model_artifacts(&mut record) {
                warn!(
                    "Model artifact refresh failed for {} (asset id {}): {error}",
                    record.path,
                    record.id.as_string()
                );
            }
        }
        next_records.push(record);
    }

    let changed = registry.records != next_records;
    registry.records = next_records;
    if changed {
        registry.index_dirty = true;
    }

    if registry.index_dirty {
        if let Err(error) = save_registry(&registry) {
            warn!("Asset registry save failed: {error}");
        } else {
            registry.index_dirty = false;
        }
    }
}

fn take_uniquely_moved_model_record(
    existing: &mut HashMap<String, AssetRecord>,
    current_paths: &HashSet<String>,
    new_path: &str,
    kind_tag: &str,
) -> Option<AssetRecord> {
    if kind_tag != "Model" {
        return None;
    }
    let bytes = std::fs::read(new_path).ok()?;
    let content_hash = blake3::hash(&bytes).to_hex().to_string();
    let candidates = existing
        .iter()
        .filter_map(|(old_path, record)| {
            if current_paths.contains(old_path) || record.kind_tag != "Model" {
                return None;
            }
            let manifest_path = record.import_settings.animation_manifest_path.as_deref()?;
            let manifest = super::animation::load_animation_manifest(manifest_path).ok()?;
            (manifest.source.fingerprint.content_hash == content_hash).then(|| old_path.clone())
        })
        .collect::<Vec<_>>();
    if candidates.len() != 1 {
        return None;
    }
    existing.remove(&candidates[0])
}

pub fn find_asset_by_path(registry: &AssetRegistry, path: &str) -> Option<AssetRecord> {
    registry
        .records
        .iter()
        .find(|record| record.path == path)
        .cloned()
}

pub fn find_asset_by_id(registry: &AssetRegistry, id: &str) -> Option<AssetRecord> {
    registry
        .records
        .iter()
        .find(|record| record.id.as_string() == id)
        .cloned()
}

pub fn find_asset_mut_by_path<'a>(
    registry: &'a mut AssetRegistry,
    path: &str,
) -> Option<&'a mut AssetRecord> {
    registry
        .records
        .iter_mut()
        .find(|record| record.path == path)
}

pub fn ensure_asset_record(
    registry: &mut AssetRegistry,
    path: impl Into<String>,
    label: impl Into<String>,
    kind_tag: impl Into<String>,
) -> Result<AssetRecord, String> {
    let path = path.into();
    if let Some(record) = find_asset_by_path(registry, &path) {
        return Ok(record);
    }
    let record = AssetRecord {
        id: AssetId::new(),
        path,
        label: label.into(),
        kind_tag: kind_tag.into(),
        import_settings: ImportSettings::default(),
        dependencies: Vec::new(),
    };
    registry.records.push(record.clone());
    registry.index_dirty = true;
    if let Err(error) = save_registry(registry) {
        registry.records.pop();
        return Err(error);
    }
    registry.index_dirty = false;
    Ok(record)
}

pub fn update_import_settings(
    registry: &mut AssetRegistry,
    path: &str,
    settings: ImportSettings,
) -> bool {
    let Some(record) = registry
        .records
        .iter_mut()
        .find(|record| record.path == path)
    else {
        return false;
    };
    record.import_settings = settings;
    registry.index_dirty = true;
    true
}

pub fn registry_index_path() -> &'static str {
    "assets/.index/registry.ron"
}

pub fn save_registry(registry: &AssetRegistry) -> Result<(), String> {
    let path = registry_index_path();
    if let Some(parent) = std::path::Path::new(path).parent() {
        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
    }
    let text = ron::ser::to_string_pretty(&registry.records, ron::ser::PrettyConfig::default())
        .map_err(|e| e.to_string())?;
    std::fs::write(path, text).map_err(|e| e.to_string())
}

pub fn load_registry() -> AssetRegistry {
    let path = registry_index_path();
    match std::fs::read_to_string(path) {
        Ok(text) => {
            let records: Vec<AssetRecord> = ron::from_str(&text).unwrap_or_default();
            AssetRegistry {
                records,
                index_dirty: false,
            }
        }
        Err(_) => AssetRegistry::default(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use shared::{
        AnimationManifest, AnimationManifestSource, AnimationSourceFingerprint,
        ANIMATION_MANIFEST_SCHEMA_VERSION,
    };

    fn moved_model_fixture(bytes: &[u8]) -> (std::path::PathBuf, std::path::PathBuf, AssetRecord) {
        let key = Uuid::new_v4();
        let source_path = std::env::temp_dir().join(format!("blacksite-moved-model-{key}.glb"));
        let manifest_path =
            std::env::temp_dir().join(format!("blacksite-moved-model-{key}.animation.ron"));
        std::fs::write(&source_path, bytes).unwrap();
        let id = AssetId::new();
        let old_path = format!("assets/models/old-{key}.glb");
        let manifest = AnimationManifest {
            schema_version: ANIMATION_MANIFEST_SCHEMA_VERSION,
            asset_id: id.as_string(),
            label: "Moved Model".into(),
            default_animation_clip_id: None,
            source: AnimationManifestSource {
                path: old_path.clone(),
                format: "glb".into(),
                fingerprint: AnimationSourceFingerprint {
                    byte_len: bytes.len() as u64,
                    modified_unix_secs: 0,
                    content_hash: blake3::hash(bytes).to_hex().to_string(),
                },
                dependencies: Vec::new(),
            },
            runtime_supported: true,
            skeletons: Vec::new(),
            clips: Vec::new(),
            diagnostics: Vec::new(),
        };
        std::fs::write(
            &manifest_path,
            ron::ser::to_string_pretty(&manifest, ron::ser::PrettyConfig::default()).unwrap(),
        )
        .unwrap();
        let record = AssetRecord {
            id,
            path: old_path,
            label: "Moved Model".into(),
            kind_tag: "Model".into(),
            import_settings: ImportSettings {
                animation_manifest_path: Some(manifest_path.to_string_lossy().into_owned()),
                ..Default::default()
            },
            dependencies: Vec::new(),
        };
        (source_path, manifest_path, record)
    }

    #[test]
    fn unique_content_match_preserves_model_record_across_move() {
        let (source_path, manifest_path, record) = moved_model_fixture(b"stable model bytes");
        let expected_id = record.id.clone();
        let mut existing = HashMap::from([(record.path.clone(), record)]);

        let moved = take_uniquely_moved_model_record(
            &mut existing,
            &HashSet::new(),
            &source_path.to_string_lossy(),
            "Model",
        )
        .expect("unique moved model should retain its registry record");

        assert_eq!(moved.id, expected_id);
        assert!(existing.is_empty());
        let _ = std::fs::remove_file(source_path);
        let _ = std::fs::remove_file(manifest_path);
    }

    #[test]
    fn existing_source_path_is_not_reconciled_as_a_copy_move() {
        let (source_path, manifest_path, record) = moved_model_fixture(b"copied model bytes");
        let old_path = record.path.clone();
        let mut existing = HashMap::from([(old_path.clone(), record)]);
        let current_paths = HashSet::from([old_path]);

        let moved = take_uniquely_moved_model_record(
            &mut existing,
            &current_paths,
            &source_path.to_string_lossy(),
            "Model",
        );

        assert!(moved.is_none());
        assert_eq!(existing.len(), 1);
        let _ = std::fs::remove_file(source_path);
        let _ = std::fs::remove_file(manifest_path);
    }
}
