//! Exact viewport targeting and reversible preview for material and texture drops.

use bevy::ecs::system::SystemParam;
use bevy::picking::mesh_picking::ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastVisibility};
use bevy::prelude::*;
use shared::{
    BrushDesc, ComponentInstanceId, EditorAssetRef, HydratedRendererMaterialBinding, LevelObject,
    MaterialDesc, MaterialInstanceAsset, MaterialRef, Primitive, SkinnedMeshRenderer,
    StaticMeshRenderer,
};

use crate::asset_db::{find_asset_by_path, AssetRegistry};
use crate::assets::materials::ensure_static_material_slots;
use crate::assets::{AssetSelection, AssetSubAssetKind, EditorAssetKind, EditorAssets};
use crate::camera::EditorCamera;
use crate::history::{
    reflected_component_transaction, set_brush_with_history, set_material_with_history,
};
use crate::infra::EditorOnly;
use crate::operators::{
    run_immediate_operator, ActiveOperator, OperatorAvailability, OperatorPhase, OperatorStatus,
};
use crate::scene_io::SceneIo;
use crate::selection::ViewportClick;
use crate::ui::selection_ops::is_mutable_level_object;
use crate::ui::UiState;
use crate::viewport::brush_edit::pick_brush_face_id;
use crate::viewport::{scene_view_ray, ViewportDisplayMode};

#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MaterialDropSet;

pub struct MaterialDropPlugin;

impl Plugin for MaterialDropPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<MaterialDropState>()
            .add_systems(Last, resolve_material_drop_target.in_set(MaterialDropSet))
            .add_systems(
                Last,
                update_material_drop_session
                    .after(resolve_material_drop_target)
                    .in_set(MaterialDropSet),
            )
            .add_systems(Update, draw_material_drop_target);
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaterialDropFeedback {
    pub valid: bool,
    pub action: String,
    pub target: String,
}

#[derive(Resource, Debug, Default)]
pub struct MaterialDropState {
    resolved: Option<MaterialDropTarget>,
    preview: Option<MaterialDropPreview>,
    feedback: Option<MaterialDropFeedback>,
    captures_viewport_input: bool,
    cancel_consumed: bool,
}

impl MaterialDropState {
    pub fn feedback(&self) -> Option<&MaterialDropFeedback> {
        self.feedback.as_ref()
    }

    pub fn captures_viewport_input(&self) -> bool {
        self.captures_viewport_input || self.cancel_consumed
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RendererKind {
    Static,
    Skinned,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum MaterialDropTargetKey {
    RendererSlot {
        owner: Entity,
        slot_id: ComponentInstanceId,
        renderer: RendererKind,
    },
    Primitive(Entity),
    BrushFace {
        owner: Entity,
        face_id: ComponentInstanceId,
    },
}

#[derive(Debug, Clone)]
enum MaterialDropTarget {
    RendererSlot {
        owner: Entity,
        owner_label: String,
        slot_id: ComponentInstanceId,
        slot_label: String,
        renderer: RendererKind,
        hit_point: Vec3,
        hit_normal: Vec3,
    },
    Primitive {
        owner: Entity,
        owner_label: String,
        hit_point: Vec3,
        hit_normal: Vec3,
    },
    BrushFace {
        owner: Entity,
        owner_label: String,
        face_id: ComponentInstanceId,
        hit_point: Vec3,
        hit_normal: Vec3,
    },
    Invalid {
        reason: String,
        hit_point: Option<Vec3>,
        hit_normal: Option<Vec3>,
    },
}

impl MaterialDropTarget {
    fn key(&self) -> Option<MaterialDropTargetKey> {
        match self {
            Self::RendererSlot {
                owner,
                slot_id,
                renderer,
                ..
            } => Some(MaterialDropTargetKey::RendererSlot {
                owner: *owner,
                slot_id: slot_id.clone(),
                renderer: *renderer,
            }),
            Self::Primitive { owner, .. } => Some(MaterialDropTargetKey::Primitive(*owner)),
            Self::BrushFace { owner, face_id, .. } => Some(MaterialDropTargetKey::BrushFace {
                owner: *owner,
                face_id: face_id.clone(),
            }),
            Self::Invalid { .. } => None,
        }
    }

    fn label(&self) -> String {
        match self {
            Self::RendererSlot {
                owner_label,
                slot_label,
                ..
            } => format!("{owner_label} / {slot_label}"),
            Self::Primitive { owner_label, .. } => format!("{owner_label} / Surface"),
            Self::BrushFace {
                owner_label,
                face_id,
                ..
            } => format!("{owner_label} / {}", face_id.0),
            Self::Invalid { reason, .. } => reason.clone(),
        }
    }

    fn hit(&self) -> Option<(Vec3, Vec3)> {
        match self {
            Self::RendererSlot {
                hit_point,
                hit_normal,
                ..
            }
            | Self::Primitive {
                hit_point,
                hit_normal,
                ..
            }
            | Self::BrushFace {
                hit_point,
                hit_normal,
                ..
            } => Some((*hit_point, *hit_normal)),
            Self::Invalid {
                hit_point,
                hit_normal,
                ..
            } => hit_point.zip(*hit_normal),
        }
    }
}

#[derive(Debug, Clone)]
enum SurfaceDropPayload {
    Material {
        label: String,
        reference: MaterialRef,
        resolved: Option<Box<MaterialDesc>>,
    },
    Texture {
        label: String,
        reference: EditorAssetRef,
        source_path: String,
    },
}

impl SurfaceDropPayload {
    fn label(&self) -> &str {
        match self {
            Self::Material { label, .. } | Self::Texture { label, .. } => label,
        }
    }

    fn kind_label(&self) -> &'static str {
        match self {
            Self::Material { .. } => "material",
            Self::Texture { .. } => "texture",
        }
    }
}

#[derive(Debug)]
struct MaterialDropPreview {
    selection: AssetSelection,
    target_key: MaterialDropTargetKey,
    label: String,
    snapshot: MaterialDropSnapshot,
}

#[derive(Debug)]
enum MaterialDropSnapshot {
    StaticRenderer {
        entity: Entity,
        before: StaticMeshRenderer,
        after: StaticMeshRenderer,
    },
    SkinnedRenderer {
        entity: Entity,
        before: SkinnedMeshRenderer,
        after: SkinnedMeshRenderer,
    },
    Material {
        entity: Entity,
        before: Option<Box<MaterialDesc>>,
        after: Box<MaterialDesc>,
    },
    Brush {
        entity: Entity,
        before: BrushDesc,
        after: BrushDesc,
    },
}

impl MaterialDropSnapshot {
    fn apply_preview(&self, world: &mut World) -> Result<(), String> {
        let mut entity = world
            .get_entity_mut(self.entity())
            .map_err(|_| "drop target no longer exists".to_string())?;
        match self {
            Self::StaticRenderer { after, .. } => {
                entity.insert(after.clone());
            }
            Self::SkinnedRenderer { after, .. } => {
                entity.insert(after.clone());
            }
            Self::Material { after, .. } => {
                entity.insert(after.as_ref().clone());
            }
            Self::Brush { after, .. } => {
                entity.insert(after.clone());
            }
        }
        Ok(())
    }

    fn restore(&self, world: &mut World) {
        let Ok(mut entity) = world.get_entity_mut(self.entity()) else {
            return;
        };
        match self {
            Self::StaticRenderer { before, .. } => {
                entity.insert(before.clone());
            }
            Self::SkinnedRenderer { before, .. } => {
                entity.insert(before.clone());
            }
            Self::Material { before, .. } => {
                if let Some(before) = before {
                    entity.insert(before.as_ref().clone());
                } else {
                    entity.remove::<MaterialDesc>();
                }
            }
            Self::Brush { before, .. } => {
                entity.insert(before.clone());
            }
        }
    }

    fn entity(&self) -> Entity {
        match self {
            Self::StaticRenderer { entity, .. }
            | Self::SkinnedRenderer { entity, .. }
            | Self::Material { entity, .. }
            | Self::Brush { entity, .. } => *entity,
        }
    }
}

#[derive(SystemParam)]
struct MaterialDropQueries<'w, 's> {
    cameras: Query<'w, 's, (&'static Camera, &'static GlobalTransform), With<EditorCamera>>,
    bindings: Query<'w, 's, &'static HydratedRendererMaterialBinding>,
    levels: Query<'w, 's, (), With<LevelObject>>,
    parents: Query<'w, 's, &'static ChildOf>,
    editor_only: Query<'w, 's, (), With<EditorOnly>>,
    brushes: Query<'w, 's, (&'static BrushDesc, &'static GlobalTransform)>,
    primitives: Query<'w, 's, (), With<Primitive>>,
    static_renderers: Query<'w, 's, &'static StaticMeshRenderer>,
    skinned_renderers: Query<'w, 's, &'static SkinnedMeshRenderer>,
    names: Query<'w, 's, &'static Name>,
}

fn resolve_material_drop_target(
    assets: Res<EditorAssets>,
    ui_state: Res<UiState>,
    display: Res<ViewportDisplayMode>,
    mut state: ResMut<MaterialDropState>,
    mut mesh_ray_cast: MeshRayCast,
    queries: MaterialDropQueries,
) {
    state.cancel_consumed = false;
    let Some(selection) = assets.dragging_selection() else {
        state.resolved = None;
        return;
    };
    if !is_surface_asset_selection_from_assets(&assets, selection)
        || display.clean_game_view
        || !ui_state.pointer_in_viewport
    {
        state.resolved = None;
        return;
    }
    let Some(pointer_pos) = ui_state.viewport_pointer_pos else {
        state.resolved = None;
        return;
    };
    let Some((camera, transform)) = queries
        .cameras
        .iter()
        .find(|(camera, _)| camera.is_active)
        .or_else(|| queries.cameras.iter().next())
    else {
        state.resolved = Some(MaterialDropTarget::Invalid {
            reason: "No active editor camera".into(),
            hit_point: None,
            hit_normal: None,
        });
        return;
    };
    let Some(ray) = scene_view_ray(camera, transform, pointer_pos, ui_state.viewport_rect) else {
        state.resolved = None;
        return;
    };
    let filter = |entity| queries.editor_only.get(entity).is_err();
    let settings = MeshRayCastSettings::default()
        .with_visibility(RayCastVisibility::Any)
        .with_filter(&filter)
        .never_early_exit();
    let hits = mesh_ray_cast.cast_ray(ray, &settings);

    for (hit_entity, hit) in hits {
        if let Ok(binding) = queries.bindings.get(*hit_entity) {
            let renderer = if queries.static_renderers.get(binding.owner).is_ok() {
                RendererKind::Static
            } else if queries.skinned_renderers.get(binding.owner).is_ok() {
                RendererKind::Skinned
            } else {
                continue;
            };
            let owner_label = entity_label(binding.owner, &queries.names);
            let slot_label = renderer_slot_label(
                binding.owner,
                &binding.slot_id,
                renderer,
                &queries.static_renderers,
                &queries.skinned_renderers,
            );
            state.resolved = Some(MaterialDropTarget::RendererSlot {
                owner: binding.owner,
                owner_label,
                slot_id: binding.slot_id.clone(),
                slot_label,
                renderer,
                hit_point: hit.point,
                hit_normal: hit.normal.normalize_or_zero(),
            });
            return;
        }

        let Some(owner) = authored_owner(*hit_entity, &queries.levels, &queries.parents) else {
            continue;
        };
        let owner_label = entity_label(owner, &queries.names);
        if let Ok((brush, transform)) = queries.brushes.get(owner) {
            let Some((face_id, _)) = pick_brush_face_id(brush, transform, ray) else {
                state.resolved = Some(MaterialDropTarget::Invalid {
                    reason: format!("No brush face under pointer on {owner_label}"),
                    hit_point: Some(hit.point),
                    hit_normal: Some(hit.normal.normalize_or_zero()),
                });
                return;
            };
            state.resolved = Some(MaterialDropTarget::BrushFace {
                owner,
                owner_label,
                face_id,
                hit_point: hit.point,
                hit_normal: hit.normal.normalize_or_zero(),
            });
            return;
        }
        if queries.primitives.get(owner).is_ok() {
            state.resolved = Some(MaterialDropTarget::Primitive {
                owner,
                owner_label,
                hit_point: hit.point,
                hit_normal: hit.normal.normalize_or_zero(),
            });
            return;
        }

        state.resolved = Some(MaterialDropTarget::Invalid {
            reason: format!("{owner_label} has no assignable surface"),
            hit_point: Some(hit.point),
            hit_normal: Some(hit.normal.normalize_or_zero()),
        });
        return;
    }

    state.resolved = Some(MaterialDropTarget::Invalid {
        reason: "No renderable surface under pointer".into(),
        hit_point: None,
        hit_normal: None,
    });
}

fn authored_owner(
    entity: Entity,
    levels: &Query<(), With<LevelObject>>,
    parents: &Query<&ChildOf>,
) -> Option<Entity> {
    let mut current = entity;
    for _ in 0..64 {
        if levels.get(current).is_ok() {
            return Some(current);
        }
        current = parents.get(current).ok()?.parent();
    }
    None
}

fn entity_label(entity: Entity, names: &Query<&Name>) -> String {
    names
        .get(entity)
        .map(|name| name.as_str().to_string())
        .unwrap_or_else(|_| format!("Actor {entity:?}"))
}

fn renderer_slot_label(
    owner: Entity,
    slot_id: &ComponentInstanceId,
    renderer: RendererKind,
    static_renderers: &Query<&StaticMeshRenderer>,
    skinned_renderers: &Query<&SkinnedMeshRenderer>,
) -> String {
    let label = match renderer {
        RendererKind::Static => static_renderers
            .get(owner)
            .ok()
            .and_then(|renderer| renderer.materials.slot(slot_id))
            .map(|slot| slot.name.as_str()),
        RendererKind::Skinned => skinned_renderers
            .get(owner)
            .ok()
            .and_then(|renderer| renderer.materials.slot(slot_id))
            .map(|slot| slot.name.as_str()),
    };
    label
        .filter(|label| !label.trim().is_empty())
        .unwrap_or(&slot_id.0)
        .to_string()
}

pub fn is_surface_asset_selection(world: &World, selection: &AssetSelection) -> bool {
    world
        .get_resource::<EditorAssets>()
        .is_some_and(|assets| is_surface_asset_selection_from_assets(assets, selection))
}

fn is_surface_asset_selection_from_assets(
    assets: &EditorAssets,
    selection: &AssetSelection,
) -> bool {
    match selection {
        AssetSelection::SubAsset {
            kind: AssetSubAssetKind::Material | AssetSubAssetKind::Texture,
            ..
        } => true,
        AssetSelection::File(_) | AssetSelection::Builtin(_) => {
            assets.asset_for_selection(selection).is_some_and(|asset| {
                matches!(
                    asset.kind,
                    EditorAssetKind::Material | EditorAssetKind::Texture
                )
            })
        }
        _ => false,
    }
}

fn surface_drop_payload(
    world: &World,
    selection: &AssetSelection,
) -> Result<SurfaceDropPayload, String> {
    let assets = world
        .get_resource::<EditorAssets>()
        .ok_or_else(|| "asset catalog is unavailable".to_string())?;
    let registry = world
        .get_resource::<AssetRegistry>()
        .ok_or_else(|| "asset registry is unavailable".to_string())?;
    match selection {
        AssetSelection::File(path) => {
            let asset = assets
                .asset_for_selection(selection)
                .ok_or_else(|| format!("asset is no longer in the catalog: {path}"))?;
            let record = find_asset_by_path(registry, path)
                .ok_or_else(|| format!("asset is missing from the registry: {path}"))?;
            match asset.kind {
                EditorAssetKind::Material => {
                    let sub_asset_id = if MaterialInstanceAsset::load_from_path(path).is_ok() {
                        "material:instance"
                    } else {
                        "material:source"
                    };
                    let reference = MaterialRef::new(
                        EditorAssetRef::new(
                            record.id.as_string(),
                            sub_asset_id,
                            asset.label.clone(),
                        )
                        .with_source_path(path.clone()),
                    );
                    let (resolved, _) = shared::load_resolved_material_from_path(path)?;
                    Ok(SurfaceDropPayload::Material {
                        label: asset.label.clone(),
                        reference,
                        resolved: Some(Box::new(resolved)),
                    })
                }
                EditorAssetKind::Texture => Ok(SurfaceDropPayload::Texture {
                    label: asset.label.clone(),
                    reference: EditorAssetRef::new(
                        record.id.as_string(),
                        "texture:source",
                        asset.label.clone(),
                    )
                    .with_source_path(path.clone()),
                    source_path: path.clone(),
                }),
                _ => Err(format!("{} is not a material or texture", asset.label)),
            }
        }
        AssetSelection::SubAsset {
            parent_path,
            sub_asset_id,
            label,
            kind: AssetSubAssetKind::Material,
            ..
        } => {
            let record = find_asset_by_path(registry, parent_path).ok_or_else(|| {
                format!("parent asset is missing from the registry: {parent_path}")
            })?;
            Ok(SurfaceDropPayload::Material {
                label: label.clone(),
                reference: MaterialRef::new(EditorAssetRef::new(
                    record.id.as_string(),
                    sub_asset_id.clone(),
                    label.clone(),
                )),
                resolved: None,
            })
        }
        AssetSelection::SubAsset {
            parent_path,
            sub_asset_id,
            label,
            kind: AssetSubAssetKind::Texture,
            source_path,
        } => {
            let record = find_asset_by_path(registry, parent_path).ok_or_else(|| {
                format!("parent asset is missing from the registry: {parent_path}")
            })?;
            let source_path = source_path
                .clone()
                .ok_or_else(|| format!("texture {label} has no source path"))?;
            Ok(SurfaceDropPayload::Texture {
                label: label.clone(),
                reference: EditorAssetRef::new(
                    record.id.as_string(),
                    sub_asset_id.clone(),
                    label.clone(),
                )
                .with_source_path(source_path.clone()),
                source_path,
            })
        }
        _ => Err("selection is not a material or texture".into()),
    }
}

fn begin_material_drop_preview(
    world: &mut World,
    selection: AssetSelection,
    target: &MaterialDropTarget,
    payload: &SurfaceDropPayload,
) -> Result<MaterialDropPreview, String> {
    let target_key = target.key().ok_or_else(|| target.label())?;
    let entity = match &target_key {
        MaterialDropTargetKey::RendererSlot { owner, .. }
        | MaterialDropTargetKey::Primitive(owner)
        | MaterialDropTargetKey::BrushFace { owner, .. } => *owner,
    };
    if !is_mutable_level_object(world, entity) {
        return Err("Target actor is locked, linked, or read-only".into());
    }

    let snapshot = match &target_key {
        MaterialDropTargetKey::RendererSlot {
            owner,
            slot_id,
            renderer: RendererKind::Static,
        } => {
            let SurfaceDropPayload::Material { reference, .. } = payload else {
                return Err("Textures cannot be assigned directly to renderer slots; create or edit a Material Instance".into());
            };
            let before = world
                .get::<StaticMeshRenderer>(*owner)
                .cloned()
                .ok_or_else(|| "static renderer no longer exists".to_string())?;
            let mut after = before.clone();
            ensure_static_material_slots(&mut after);
            let slot = after
                .materials
                .slot_mut(slot_id)
                .ok_or_else(|| format!("renderer slot {} no longer exists", slot_id.0))?;
            slot.material = Some(reference.clone());
            MaterialDropSnapshot::StaticRenderer {
                entity: *owner,
                before,
                after,
            }
        }
        MaterialDropTargetKey::RendererSlot {
            owner,
            slot_id,
            renderer: RendererKind::Skinned,
        } => {
            let SurfaceDropPayload::Material { reference, .. } = payload else {
                return Err("Textures cannot be assigned directly to renderer slots; create or edit a Material Instance".into());
            };
            let before = world
                .get::<SkinnedMeshRenderer>(*owner)
                .cloned()
                .ok_or_else(|| "skinned renderer no longer exists".to_string())?;
            let mut after = before.clone();
            let slot = after
                .materials
                .slot_mut(slot_id)
                .ok_or_else(|| format!("renderer slot {} no longer exists", slot_id.0))?;
            slot.material = Some(reference.clone());
            MaterialDropSnapshot::SkinnedRenderer {
                entity: *owner,
                before,
                after,
            }
        }
        MaterialDropTargetKey::Primitive(owner) => {
            if world.get::<Primitive>(*owner).is_none() {
                return Err("primitive target no longer exists".into());
            }
            let before = world.get::<MaterialDesc>(*owner).cloned();
            let after = material_desc_for_surface(before.clone(), payload)?;
            MaterialDropSnapshot::Material {
                entity: *owner,
                before: before.map(Box::new),
                after: Box::new(after),
            }
        }
        MaterialDropTargetKey::BrushFace { owner, face_id } => {
            let before = world
                .get::<BrushDesc>(*owner)
                .cloned()
                .ok_or_else(|| "brush target no longer exists".to_string())?;
            let mut after = before.clone();
            let face = after
                .faces
                .iter_mut()
                .find(|face| &face.id == face_id)
                .ok_or_else(|| format!("brush face {} no longer exists", face_id.0))?;
            match payload {
                SurfaceDropPayload::Material {
                    reference,
                    resolved,
                    ..
                } => {
                    if resolved.is_none() || reference.0.source_path.is_none() {
                        return Err(
                            "Embedded source materials can only be assigned to renderer slots"
                                .into(),
                        );
                    }
                    face.material = Some(reference.0.clone());
                }
                SurfaceDropPayload::Texture { reference, .. } => {
                    face.texture = Some(reference.clone());
                }
            }
            MaterialDropSnapshot::Brush {
                entity: *owner,
                before,
                after,
            }
        }
    };
    snapshot.apply_preview(world)?;
    Ok(MaterialDropPreview {
        selection,
        target_key,
        label: format!("Assign {} to {}", payload.label(), target.label()),
        snapshot,
    })
}

fn material_desc_for_surface(
    before: Option<MaterialDesc>,
    payload: &SurfaceDropPayload,
) -> Result<MaterialDesc, String> {
    match payload {
        SurfaceDropPayload::Material {
            resolved: Some(desc),
            ..
        } => Ok(desc.as_ref().clone()),
        SurfaceDropPayload::Material { resolved: None, .. } => {
            Err("Embedded source materials can only be assigned to renderer slots".into())
        }
        SurfaceDropPayload::Texture { source_path, .. } => {
            let mut material = before.unwrap_or_default();
            material.base_color_texture = Some(source_path.clone());
            Ok(material)
        }
    }
}

fn update_material_drop_session(world: &mut World) {
    let mut state = world
        .remove_resource::<MaterialDropState>()
        .unwrap_or_default();
    let selection = world
        .get_resource::<EditorAssets>()
        .and_then(|assets| assets.dragging_selection().cloned())
        .filter(|selection| is_surface_asset_selection(world, selection));
    let primary_released = world
        .get_resource::<ButtonInput<MouseButton>>()
        .is_some_and(|buttons| buttons.just_released(MouseButton::Left));
    let cancel_requested = world
        .get_resource::<ButtonInput<KeyCode>>()
        .is_some_and(|keys| keys.just_pressed(KeyCode::Escape))
        || world
            .get_resource::<ButtonInput<MouseButton>>()
            .is_some_and(|buttons| buttons.just_pressed(MouseButton::Right));

    if cancel_requested && (selection.is_some() || state.preview.is_some()) {
        restore_material_drop_preview(world, state.preview.take());
        clear_drag_and_viewport_click(world);
        state.feedback = None;
        state.captures_viewport_input = false;
        state.cancel_consumed = true;
        set_drop_operator_status(
            world,
            OperatorPhase::Canceled,
            "Surface assignment canceled",
        );
        world.insert_resource(state);
        return;
    }

    let Some(selection) = selection else {
        let had_preview = state.preview.is_some();
        restore_material_drop_preview(world, state.preview.take());
        state.feedback = None;
        state.captures_viewport_input = false;
        if had_preview {
            set_drop_operator_status(
                world,
                OperatorPhase::Canceled,
                "Surface assignment canceled",
            );
        }
        world.insert_resource(state);
        return;
    };
    state.captures_viewport_input = true;

    let Some(target) = state.resolved.clone() else {
        restore_material_drop_preview(world, state.preview.take());
        state.feedback = None;
        if primary_released {
            clear_drag_and_viewport_click(world);
            state.captures_viewport_input = false;
        }
        world.insert_resource(state);
        return;
    };

    let payload = match surface_drop_payload(world, &selection) {
        Ok(payload) => payload,
        Err(error) => {
            restore_material_drop_preview(world, state.preview.take());
            state.feedback = Some(MaterialDropFeedback {
                valid: false,
                action: error.clone(),
                target: target.label(),
            });
            set_drop_operator_status(world, OperatorPhase::Blocked, error);
            if primary_released {
                clear_drag_and_viewport_click(world);
                state.captures_viewport_input = false;
            }
            world.insert_resource(state);
            return;
        }
    };

    let target_key = target.key();
    let preview_matches = state.preview.as_ref().is_some_and(|preview| {
        preview.selection == selection && Some(&preview.target_key) == target_key.as_ref()
    });
    if !preview_matches {
        restore_material_drop_preview(world, state.preview.take());
        match begin_material_drop_preview(world, selection.clone(), &target, &payload) {
            Ok(preview) => state.preview = Some(preview),
            Err(error) => {
                state.feedback = Some(MaterialDropFeedback {
                    valid: false,
                    action: error.clone(),
                    target: target.label(),
                });
                set_drop_operator_status(world, OperatorPhase::Blocked, error);
                if primary_released {
                    clear_drag_and_viewport_click(world);
                    state.captures_viewport_input = false;
                }
                world.insert_resource(state);
                return;
            }
        }
    }

    state.feedback = Some(MaterialDropFeedback {
        valid: true,
        action: format!("Assign {} to {}", payload.kind_label(), target.label()),
        target: target.label(),
    });
    set_drop_operator_status(
        world,
        OperatorPhase::Preview,
        format!("Release to assign {}", payload.kind_label()),
    );

    if primary_released {
        let preview = state.preview.take();
        if let Some(preview) = preview.as_ref() {
            preview.snapshot.restore(world);
        }
        if let Some(preview) = preview {
            commit_material_drop_preview(world, preview);
        }
        clear_drag_and_viewport_click(world);
        state.captures_viewport_input = false;
    }
    world.insert_resource(state);
}

fn restore_material_drop_preview(world: &mut World, preview: Option<MaterialDropPreview>) {
    if let Some(preview) = preview {
        preview.snapshot.restore(world);
    }
}

fn commit_material_drop_preview(world: &mut World, preview: MaterialDropPreview) -> bool {
    let label = preview.label.clone();
    let snapshot = preview.snapshot;
    let committed = run_immediate_operator(
        world,
        "assets.assign_surface",
        &label,
        OperatorAvailability::Ready,
        move |world| commit_material_drop_snapshot(world, snapshot),
    );
    if committed {
        if let Some(mut io) = world.get_resource_mut::<SceneIo>() {
            io.status = label;
        }
    }
    committed
}

fn commit_material_drop_snapshot(
    world: &mut World,
    snapshot: MaterialDropSnapshot,
) -> Result<(), String> {
    let entity = snapshot.entity();
    if !is_mutable_level_object(world, entity) {
        return Err("Target actor is no longer editable".into());
    }
    match snapshot {
        MaterialDropSnapshot::StaticRenderer { after, .. } => reflected_component_transaction(
            world,
            entity,
            "Assign Renderer Material Slot",
            shared::AUTHORING_COMPONENT_STATIC_MESH_RENDERER,
            shared::COMPONENT_STATIC_MESH_RENDERER,
            move |world, entity| {
                world.entity_mut(entity).insert(after);
                Ok(())
            },
        ),
        MaterialDropSnapshot::SkinnedRenderer { after, .. } => reflected_component_transaction(
            world,
            entity,
            "Assign Renderer Material Slot",
            shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER,
            shared::COMPONENT_SKINNED_MESH_RENDERER,
            move |world, entity| {
                world.entity_mut(entity).insert(after);
                Ok(())
            },
        ),
        MaterialDropSnapshot::Material { after, .. } => {
            set_material_with_history(world, entity, *after);
            Ok(())
        }
        MaterialDropSnapshot::Brush { after, .. } => {
            set_brush_with_history(world, entity, after);
            Ok(())
        }
    }
}

fn clear_drag_and_viewport_click(world: &mut World) {
    if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
        assets.clear_drag();
    }
    if let Some(mut click) = world.get_resource_mut::<ViewportClick>() {
        click.0 = None;
    }
}

fn set_drop_operator_status(world: &mut World, phase: OperatorPhase, hint: impl Into<String>) {
    if let Some(mut active) = world.get_resource_mut::<ActiveOperator>() {
        active.status = Some(OperatorStatus {
            id: "assets.assign_surface".into(),
            label: "Assign Surface Asset".into(),
            phase,
            hint: hint.into(),
            warnings: Vec::new(),
        });
    }
}

fn draw_material_drop_target(
    state: Res<MaterialDropState>,
    brushes: Query<(&BrushDesc, &GlobalTransform)>,
    mut gizmos: Gizmos,
) {
    if !state.captures_viewport_input {
        return;
    }
    let Some(target) = state.resolved.as_ref() else {
        return;
    };
    let valid = state
        .feedback
        .as_ref()
        .is_some_and(|feedback| feedback.valid);
    let color = if valid {
        Color::srgba(0.33, 0.9, 0.58, 0.98)
    } else {
        Color::srgba(0.95, 0.3, 0.34, 0.98)
    };
    if let Some((point, normal)) = target.hit() {
        gizmos.sphere(point, 0.09, color);
        if normal.length_squared() > 0.001 {
            gizmos.line(point, point + normal * 0.42, color);
        }
    }
    let MaterialDropTarget::BrushFace { owner, face_id, .. } = target else {
        return;
    };
    let Ok((brush, transform)) = brushes.get(*owner) else {
        return;
    };
    let Some(face) = brush.faces.iter().find(|face| &face.id == face_id) else {
        return;
    };
    let vertices = face
        .vertices
        .iter()
        .map(|vertex| transform.transform_point(*vertex))
        .collect::<Vec<_>>();
    for index in 0..vertices.len() {
        gizmos.line(
            vertices[index],
            vertices[(index + 1) % vertices.len()],
            color,
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::history::{apply_command_undo, EditorHistory};
    use shared::{ActorKind, RendererMaterialSet, RendererMaterialSlot, SharedTypesPlugin};

    fn test_app() -> App {
        let mut app = App::new();
        app.add_plugins(SharedTypesPlugin);
        let world = app.world_mut();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        world.init_resource::<ActiveOperator>();
        app
    }

    fn material_payload(id: &str) -> SurfaceDropPayload {
        SurfaceDropPayload::Material {
            label: "Steel".into(),
            reference: MaterialRef::new(
                EditorAssetRef::new(id, "material:source", "Steel")
                    .with_source_path("assets/materials/steel.ron"),
            ),
            resolved: Some(Box::new(MaterialDesc {
                roughness: 0.2,
                material_asset_path: Some("assets/materials/steel.ron".into()),
                ..Default::default()
            })),
        }
    }

    #[test]
    fn renderer_preview_and_commit_touch_only_the_hit_slot() {
        let mut app = test_app();
        let world = app.world_mut();
        let slot_a = ComponentInstanceId::new("slot:a");
        let slot_b = ComponentInstanceId::new("slot:b");
        let entity = world
            .spawn((
                LevelObject,
                ActorKind::StaticMesh,
                StaticMeshRenderer {
                    slots: Vec::new(),
                    materials: RendererMaterialSet {
                        slots: vec![
                            RendererMaterialSlot {
                                id: slot_a.clone(),
                                name: "A".into(),
                                source_material: None,
                                material: None,
                            },
                            RendererMaterialSlot {
                                id: slot_b.clone(),
                                name: "B".into(),
                                source_material: None,
                                material: None,
                            },
                        ],
                        orphaned_assignments: Vec::new(),
                    },
                },
            ))
            .id();
        let target = MaterialDropTarget::RendererSlot {
            owner: entity,
            owner_label: "Crate".into(),
            slot_id: slot_b.clone(),
            slot_label: "B".into(),
            renderer: RendererKind::Static,
            hit_point: Vec3::ZERO,
            hit_normal: Vec3::Y,
        };
        let preview = begin_material_drop_preview(
            world,
            AssetSelection::File("assets/materials/steel.ron".into()),
            &target,
            &material_payload("steel"),
        )
        .unwrap();

        let renderer = world.get::<StaticMeshRenderer>(entity).unwrap();
        assert!(renderer.materials.slot(&slot_a).unwrap().material.is_none());
        assert_eq!(
            renderer
                .materials
                .slot(&slot_b)
                .unwrap()
                .material
                .as_ref()
                .unwrap()
                .0
                .asset_id,
            "steel"
        );
        assert!(!world.resource::<SceneIo>().dirty);
        assert_eq!(world.resource::<EditorHistory>().undo_depth(), 0);

        preview.snapshot.restore(world);
        assert!(world
            .get::<StaticMeshRenderer>(entity)
            .unwrap()
            .materials
            .slot(&slot_b)
            .unwrap()
            .material
            .is_none());
        assert!(commit_material_drop_preview(world, preview));
        assert_eq!(world.resource::<EditorHistory>().undo_depth(), 1);
        assert!(world.resource::<SceneIo>().dirty);
        assert!(world
            .get::<StaticMeshRenderer>(entity)
            .unwrap()
            .materials
            .slot(&slot_a)
            .unwrap()
            .material
            .is_none());

        apply_command_undo(world);
        assert!(world
            .get::<StaticMeshRenderer>(entity)
            .unwrap()
            .materials
            .slot(&slot_b)
            .unwrap()
            .material
            .is_none());
    }

    #[test]
    fn brush_preview_restores_and_targets_only_one_face() {
        let mut app = test_app();
        let world = app.world_mut();
        let brush = BrushDesc::cuboid(Vec3::ONE);
        let target_face = brush.faces[2].id.clone();
        let untouched_face = brush.faces[3].id.clone();
        let entity = world
            .spawn((LevelObject, ActorKind::Brush, brush.clone()))
            .id();
        let target = MaterialDropTarget::BrushFace {
            owner: entity,
            owner_label: "Blockout".into(),
            face_id: target_face.clone(),
            hit_point: Vec3::ZERO,
            hit_normal: Vec3::Y,
        };
        let preview = begin_material_drop_preview(
            world,
            AssetSelection::File("assets/materials/steel.ron".into()),
            &target,
            &material_payload("steel"),
        )
        .unwrap();

        let previewed = world.get::<BrushDesc>(entity).unwrap();
        assert!(previewed
            .faces
            .iter()
            .find(|face| face.id == target_face)
            .unwrap()
            .material
            .is_some());
        assert!(previewed
            .faces
            .iter()
            .find(|face| face.id == untouched_face)
            .unwrap()
            .material
            .is_none());

        preview.snapshot.restore(world);
        assert_eq!(world.get::<BrushDesc>(entity), Some(&brush));
        assert!(!world.resource::<SceneIo>().dirty);
    }

    #[test]
    fn texture_drop_rejects_renderer_slots_without_mutating_them() {
        let mut app = test_app();
        let world = app.world_mut();
        let slot_id = ComponentInstanceId::new("slot:body");
        let renderer = StaticMeshRenderer {
            slots: Vec::new(),
            materials: RendererMaterialSet {
                slots: vec![RendererMaterialSlot {
                    id: slot_id.clone(),
                    name: "Body".into(),
                    source_material: None,
                    material: None,
                }],
                orphaned_assignments: Vec::new(),
            },
        };
        let entity = world
            .spawn((LevelObject, ActorKind::StaticMesh, renderer.clone()))
            .id();
        let target = MaterialDropTarget::RendererSlot {
            owner: entity,
            owner_label: "Crate".into(),
            slot_id,
            slot_label: "Body".into(),
            renderer: RendererKind::Static,
            hit_point: Vec3::ZERO,
            hit_normal: Vec3::Y,
        };
        let payload = SurfaceDropPayload::Texture {
            label: "Grid".into(),
            reference: EditorAssetRef::new("grid", "texture:source", "Grid")
                .with_source_path("assets/textures/grid.png"),
            source_path: "assets/textures/grid.png".into(),
        };

        let error = begin_material_drop_preview(
            world,
            AssetSelection::File("assets/textures/grid.png".into()),
            &target,
            &payload,
        )
        .unwrap_err();

        assert!(error.contains("Material Instance"));
        assert_eq!(world.get::<StaticMeshRenderer>(entity), Some(&renderer));
        assert_eq!(world.resource::<EditorHistory>().undo_depth(), 0);
    }

    #[test]
    fn resolved_project_material_is_supported_for_primitives() {
        let mut material = material_desc_for_surface(None, &material_payload("steel")).unwrap();
        assert_eq!(material.roughness, 0.2);
        assert_eq!(
            material.material_asset_path.as_deref(),
            Some("assets/materials/steel.ron")
        );
        material.base_color_texture = None;
        let texture = SurfaceDropPayload::Texture {
            label: "Grid".into(),
            reference: EditorAssetRef::default(),
            source_path: "assets/textures/grid.png".into(),
        };
        let textured = material_desc_for_surface(Some(material), &texture).unwrap();
        assert_eq!(
            textured.base_color_texture.as_deref(),
            Some("assets/textures/grid.png")
        );
    }

    #[test]
    fn locked_target_rejects_preview_without_mutation_or_history() {
        let mut app = test_app();
        let world = app.world_mut();
        let original = MaterialDesc::default();
        let entity = world
            .spawn((
                LevelObject,
                ActorKind::Empty,
                Primitive::default(),
                original.clone(),
            ))
            .id();
        let mut hierarchy = crate::ui::hierarchy_state::HierarchyPanelState::default();
        hierarchy.locked.insert(entity);
        world.insert_resource(hierarchy);
        let target = MaterialDropTarget::Primitive {
            owner: entity,
            owner_label: "Locked Cube".into(),
            hit_point: Vec3::ZERO,
            hit_normal: Vec3::Y,
        };

        let error = begin_material_drop_preview(
            world,
            AssetSelection::File("assets/materials/steel.ron".into()),
            &target,
            &material_payload("steel"),
        )
        .unwrap_err();

        assert!(error.contains("locked"));
        assert_eq!(world.get::<MaterialDesc>(entity), Some(&original));
        assert_eq!(world.resource::<EditorHistory>().undo_depth(), 0);
        assert!(!world.resource::<SceneIo>().dirty);
    }
}
