//! Operator wrappers for asset placement and assignment workflows.

use bevy::prelude::*;
use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;

use crate::operators::{run_immediate_operator, OperatorAvailability};
use crate::ui::helpers::{
    apply_material_asset_to_selection, apply_texture_to_selection, selected_level_entities,
};

use super::{
    animation_clip_authoring_data, animation_skeleton_signature_for_ref, audio_clip_ref_for_asset,
    compatible_animation_skeleton_for_model, spawn_asset_at, spawn_subasset_at,
    validate_prefab_asset, AssetSelection, EditorAsset,
};

fn selection_availability(world: &World, selected: &SelectedEntities) -> OperatorAvailability {
    if selected_level_entities(world, selected).is_empty() {
        OperatorAvailability::Disabled("Select at least one authored actor".to_string())
    } else {
        OperatorAvailability::Ready
    }
}

pub fn place_asset_operator(world: &mut World, asset: EditorAsset, translation: Vec3) -> bool {
    let label = format!("Place {}", asset.label);
    run_immediate_operator(
        world,
        "assets.place",
        &label,
        OperatorAvailability::Ready,
        move |world| {
            validate_prefab_asset(world, &asset)?;
            spawn_asset_at(world, &asset, translation)
                .map(|_| ())
                .ok_or_else(|| format!("Asset cannot be placed: {}", asset.label))
        },
    )
}

pub fn place_subasset_operator(
    world: &mut World,
    selection: AssetSelection,
    translation: Vec3,
) -> bool {
    let label = format!("Place {}", selection.display_label());
    run_immediate_operator(
        world,
        "assets.place_subasset",
        &label,
        OperatorAvailability::Ready,
        move |world| {
            spawn_subasset_at(world, &selection, translation)
                .map(|_| ())
                .ok_or_else(|| format!("Sub-asset cannot be placed: {}", selection.display_label()))
        },
    )
}

pub fn apply_texture_operator(
    world: &mut World,
    asset: EditorAsset,
    selected: &SelectedEntities,
) -> bool {
    let label = format!("Apply Texture {}", asset.label);
    let availability = selection_availability(world, selected);
    run_immediate_operator(
        world,
        "assets.apply_texture",
        &label,
        availability,
        move |world| apply_texture_to_selection(world, &asset, selected),
    )
}

pub fn apply_material_operator(
    world: &mut World,
    asset: EditorAsset,
    selected: &SelectedEntities,
) -> bool {
    let label = format!("Apply Material {}", asset.label);
    let availability = selection_availability(world, selected);
    run_immediate_operator(
        world,
        "assets.apply_material",
        &label,
        availability,
        move |world| apply_material_asset_to_selection(world, &asset, selected),
    )
}

pub fn assign_audio_clip_operator(world: &mut World, asset: EditorAsset, entity: Entity) -> bool {
    let availability = if world.get::<shared::AudioSourceDesc>(entity).is_some() {
        OperatorAvailability::Ready
    } else {
        OperatorAvailability::Disabled("Select an authored audio source".to_string())
    };
    let label = format!("Assign Audio Clip {}", asset.label);
    run_immediate_operator(
        world,
        "assets.assign_audio_clip",
        &label,
        availability,
        move |world| {
            let clip = audio_clip_ref_for_asset(world, &asset)?;
            let mut source = world
                .get::<shared::AudioSourceDesc>(entity)
                .cloned()
                .ok_or_else(|| "Selected actor is not an audio source".to_string())?;
            source.clip = Some(clip);
            crate::history::set_audio_source_with_history(world, entity, source);
            Ok(())
        },
    )
}

pub fn assign_animation_clip_operator(
    world: &mut World,
    selection: AssetSelection,
    entity: Entity,
) -> bool {
    let availability = if world.get::<shared::SkinnedMeshRenderer>(entity).is_some() {
        OperatorAvailability::Ready
    } else {
        OperatorAvailability::Disabled("Select an authored skinned-mesh actor".to_string())
    };
    let label = format!("Assign Animation Clip {}", selection.display_label());
    run_immediate_operator(
        world,
        "assets.assign_animation_clip",
        &label,
        availability,
        move |world| {
            let authored = animation_clip_authoring_data(world, &selection)?;
            let model = world
                .get::<shared::SkinnedMeshRenderer>(entity)
                .cloned()
                .ok_or_else(|| "Selected actor has no SkinnedMeshRenderer".to_string())?;
            let mut controller = world
                .get::<shared::AnimationControllerDesc>(entity)
                .cloned()
                .unwrap_or_default();

            let skeleton = match controller.skeleton.as_ref() {
                Some(reference) => {
                    let existing_signature =
                        animation_skeleton_signature_for_ref(world, reference)?;
                    if existing_signature != authored.skeleton_signature {
                        return Err(format!(
                            "Clip `{}` targets rig `{}` but the actor uses `{}`. Animation v1 requires an exact skeleton match.",
                            authored.state.label,
                            authored.skeleton_signature.0,
                            existing_signature.0
                        ));
                    }
                    reference.clone()
                }
                None => compatible_animation_skeleton_for_model(
                    world,
                    &model,
                    &authored.skeleton_signature,
                )?,
            };

            if controller.states.iter().any(|state| {
                state.clip.asset_id == authored.state.clip.asset_id
                    && state.clip.sub_asset_id == authored.state.clip.sub_asset_id
            }) {
                return Err(format!(
                    "Animation controller already contains clip `{}`.",
                    authored.state.label
                ));
            }

            let mut state = authored.state;
            let base_id = state.id.clone();
            let mut suffix = 2;
            while controller.states.iter().any(|entry| entry.id == state.id) {
                state.id = format!("{base_id}_{suffix}");
                suffix += 1;
            }
            controller.skeleton = Some(skeleton);
            if controller.default_state.trim().is_empty() {
                controller.default_state = state.id.clone();
            }
            controller.states.push(state);
            crate::history::set_animation_controller_with_history(world, entity, controller);
            Ok(())
        },
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use shared::{
        animation_clip_sub_asset_id, animation_skeleton_sub_asset_id, ActorKind,
        AnimationClipRecord, AnimationControllerDesc, AnimationManifest, AnimationManifestSource,
        AnimationSkeletonRecord, AnimationSkeletonSignature, AnimationSourceFingerprint,
        AudioSourceDesc, LevelObject, MaterialDesc, PrimitiveShape, SkinnedMeshRenderer,
        ANIMATION_MANIFEST_SCHEMA_VERSION,
    };

    use crate::asset_db::{AssetId, AssetRecord, AssetRegistry, ImportSettings};
    use crate::history::EditorHistory;
    use crate::operators::test_harness::{assert_undo_redo_round_trip, OperatorInvariantHarness};
    use crate::operators::ActiveOperator;
    use crate::scene_io::SceneIo;
    use crate::selection::SelectedEntity;

    fn authored_count(world: &mut World) -> usize {
        let mut query = world.query_filtered::<Entity, With<LevelObject>>();
        query.iter(world).count()
    }

    fn animation_fixture() -> (std::path::PathBuf, AssetRecord, AssetSelection) {
        let model_path = "assets/models/operator_fixture.glb".to_string();
        let asset_id = AssetId::new();
        let skeleton_id = animation_skeleton_sub_asset_id(0, "Rig");
        let clip_id = animation_clip_sub_asset_id(0, "Idle");
        let manifest = AnimationManifest {
            schema_version: ANIMATION_MANIFEST_SCHEMA_VERSION,
            asset_id: asset_id.as_string(),
            label: "Operator Fixture".into(),
            default_animation_clip_id: None,
            source: AnimationManifestSource {
                path: model_path.clone(),
                format: "glb".into(),
                fingerprint: AnimationSourceFingerprint {
                    byte_len: 1,
                    modified_unix_secs: 0,
                    content_hash: "fixture".into(),
                },
                dependencies: Vec::new(),
            },
            runtime_supported: true,
            skeletons: vec![AnimationSkeletonRecord {
                id: skeleton_id,
                label: "Rig".into(),
                source_index: 0,
                signature: AnimationSkeletonSignature::new("exact-rig"),
                joint_paths: vec!["Root".into()],
            }],
            clips: vec![AnimationClipRecord {
                id: clip_id.clone(),
                label: "Idle".into(),
                source_index: 0,
                duration_seconds: 1.0,
                target_skeleton_signature: Some(AnimationSkeletonSignature::new("exact-rig")),
                events: Vec::new(),
            }],
            diagnostics: Vec::new(),
        };
        let path = std::env::temp_dir().join(format!(
            "blacksite-animation-operator-{}.animation.ron",
            uuid::Uuid::new_v4()
        ));
        std::fs::write(
            &path,
            ron::ser::to_string_pretty(&manifest, ron::ser::PrettyConfig::default()).unwrap(),
        )
        .unwrap();
        let record = AssetRecord {
            id: asset_id,
            path: model_path.clone(),
            label: "Operator Fixture".into(),
            kind_tag: "Model".into(),
            import_settings: ImportSettings {
                animation_manifest_path: Some(path.to_string_lossy().into_owned()),
                ..Default::default()
            },
            dependencies: Vec::new(),
        };
        let selection = AssetSelection::SubAsset {
            parent_path: model_path.clone(),
            sub_asset_id: clip_id,
            label: "Idle".into(),
            kind: super::super::AssetSubAssetKind::AnimationClip,
            source_path: Some(model_path),
        };
        (path, record, selection)
    }

    #[test]
    fn asset_placement_commits_one_undo_group_and_round_trips() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        world.init_resource::<SelectedEntity>();
        let harness = OperatorInvariantHarness::capture(&mut world);
        let asset = EditorAsset {
            label: "Cube".to_string(),
            path: None,
            folder_path: "Built-ins".to_string(),
            kind: super::super::EditorAssetKind::Primitive(PrimitiveShape::Box),
        };

        assert!(place_asset_operator(&mut world, asset, Vec3::ZERO));

        harness.assert_committed(&mut world, 1, 1);
        assert_undo_redo_round_trip(&mut world, 0, 1, authored_count);
    }

    #[test]
    fn animation_clip_placement_creates_only_the_skinned_renderer_path() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        world.init_resource::<SelectedEntity>();
        let (manifest_path, record, selection) = animation_fixture();
        world.insert_resource(AssetRegistry {
            records: vec![record],
            index_dirty: false,
        });
        let harness = OperatorInvariantHarness::capture(&mut world);

        assert!(place_subasset_operator(
            &mut world,
            selection,
            Vec3::new(1.0, 2.0, 3.0),
        ));

        let entity = world
            .query_filtered::<Entity, With<LevelObject>>()
            .single(&world)
            .unwrap();
        assert_eq!(
            world.get::<ActorKind>(entity),
            Some(&ActorKind::SkinnedMesh)
        );
        assert!(world.get::<SkinnedMeshRenderer>(entity).is_some());
        assert!(world.get::<AnimationControllerDesc>(entity).is_some());
        assert!(world.get::<shared::ModelRef>(entity).is_none());
        assert!(world.get::<shared::StaticMeshRenderer>(entity).is_none());
        harness.assert_committed(&mut world, 1, 1);
        assert_undo_redo_round_trip(&mut world, 0, 1, authored_count);
        let _ = std::fs::remove_file(manifest_path);
    }

    #[test]
    fn audio_clip_assignment_is_one_undoable_operator() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        let path = "assets/audio/impact.ogg".to_string();
        world.insert_resource(AssetRegistry {
            records: vec![AssetRecord {
                id: AssetId::new(),
                path: path.clone(),
                label: "Impact".into(),
                kind_tag: "AudioClip".into(),
                import_settings: Default::default(),
                dependencies: Vec::new(),
            }],
            index_dirty: false,
        });
        let entity = world
            .spawn((
                LevelObject,
                ActorKind::AudioSource,
                Transform::IDENTITY,
                AudioSourceDesc::default(),
            ))
            .id();
        let asset = EditorAsset {
            label: "Impact".into(),
            path: Some(path.clone()),
            folder_path: "assets/audio".into(),
            kind: super::super::EditorAssetKind::AudioClip,
        };
        let harness = OperatorInvariantHarness::capture(&mut world);
        let read_clip = move |world: &mut World| {
            world
                .get::<AudioSourceDesc>(entity)
                .and_then(|source| source.clip.clone())
        };

        assert!(assign_audio_clip_operator(&mut world, asset, entity));

        let assigned = read_clip(&mut world).expect("clip assigned");
        assert_eq!(assigned.source_path.as_deref(), Some(path.as_str()));
        harness.assert_committed(&mut world, 1, 0);
        assert_undo_redo_round_trip(&mut world, None, Some(assigned), read_clip);
    }

    #[test]
    fn animation_clip_assignment_is_one_undoable_operator() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        let (manifest_path, record, selection) = animation_fixture();
        let model_path = record.path.clone();
        world.insert_resource(AssetRegistry {
            records: vec![record],
            index_dirty: false,
        });
        let entity = world
            .spawn((
                LevelObject,
                ActorKind::SkinnedMesh,
                SkinnedMeshRenderer::new(model_path),
            ))
            .id();
        let harness = OperatorInvariantHarness::capture(&mut world);
        let read_controller =
            move |world: &mut World| world.get::<AnimationControllerDesc>(entity).cloned();

        assert!(assign_animation_clip_operator(
            &mut world, selection, entity
        ));

        let assigned = read_controller(&mut world).expect("controller assigned");
        assert_eq!(assigned.default_state, "idle");
        assert_eq!(assigned.states.len(), 1);
        assert_eq!(assigned.states[0].id, "idle");
        assert_eq!(
            assigned.skeleton.as_ref().unwrap().sub_asset_id,
            animation_skeleton_sub_asset_id(0, "Rig")
        );
        harness.assert_committed(&mut world, 1, 0);
        assert_undo_redo_round_trip(&mut world, None, Some(assigned), read_controller);
        let _ = std::fs::remove_file(manifest_path);
    }

    #[test]
    fn multi_actor_texture_assignment_is_one_undo_group() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        let first = world.spawn((LevelObject, MaterialDesc::default())).id();
        let second = world.spawn((LevelObject, MaterialDesc::default())).id();
        let mut selected = SelectedEntities::default();
        selected.select_replace(first);
        selected.select_maybe_add(second, true);
        let asset = EditorAsset {
            label: "Grid".to_string(),
            path: Some("assets/textures/grid.png".to_string()),
            folder_path: "assets/textures".to_string(),
            kind: super::super::EditorAssetKind::Texture,
        };
        let harness = OperatorInvariantHarness::capture(&mut world);
        let read_textures = move |world: &mut World| {
            [first, second]
                .into_iter()
                .map(|entity| {
                    world
                        .get::<MaterialDesc>(entity)
                        .and_then(|material| material.base_color_texture.clone())
                })
                .collect::<Vec<_>>()
        };

        assert!(apply_texture_operator(&mut world, asset, &selected));

        harness.assert_committed(&mut world, 1, 0);
        assert_undo_redo_round_trip(
            &mut world,
            vec![None, None],
            vec![
                Some("assets/textures/grid.png".to_string()),
                Some("assets/textures/grid.png".to_string()),
            ],
            read_textures,
        );
    }

    #[test]
    fn missing_subasset_commit_cancels_without_side_effects() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        let harness = OperatorInvariantHarness::capture(&mut world);
        let selection = AssetSelection::SubAsset {
            parent_path: "assets/models/missing.glb".to_string(),
            sub_asset_id: "mesh:missing".to_string(),
            label: "Missing Mesh".to_string(),
            kind: super::super::AssetSubAssetKind::Mesh,
            source_path: None,
        };

        assert!(!place_subasset_operator(&mut world, selection, Vec3::ZERO));

        harness.assert_canceled(&mut world);
    }

    #[test]
    fn missing_material_source_cancels_without_side_effects() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        let entity = world.spawn(LevelObject).id();
        let mut selected = SelectedEntities::default();
        selected.select_replace(entity);
        let harness = OperatorInvariantHarness::capture(&mut world);
        let asset = EditorAsset {
            label: "Missing Material".to_string(),
            path: Some("assets/materials/does-not-exist.material.ron".to_string()),
            folder_path: "assets/materials".to_string(),
            kind: super::super::EditorAssetKind::Material,
        };

        assert!(!apply_material_operator(&mut world, asset, &selected));

        harness.assert_canceled(&mut world);
    }

    #[test]
    fn texture_assignment_without_authored_selection_is_blocked() {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        let harness = OperatorInvariantHarness::capture(&mut world);
        let asset = EditorAsset {
            label: "Grid".to_string(),
            path: Some("assets/textures/grid.png".to_string()),
            folder_path: "assets/textures".to_string(),
            kind: super::super::EditorAssetKind::Texture,
        };

        assert!(!apply_texture_operator(
            &mut world,
            asset,
            &SelectedEntities::default()
        ));

        harness.assert_blocked(&mut world);
    }
}
