use crate::ecs::generational_registry::registry_entry_by_name;
use crate::ecs::prefab::resources::mesh_cache_insert;
use crate::ecs::prefab::{import_gltf_from_path, spawn_prefab};
use crate::ecs::transform::components::LocalTransformDirty;
use crate::ecs::world::commands::{
    set_material_with_textures, spawn_cone_at, spawn_cube_at, spawn_cylinder_at, spawn_plane_at,
    spawn_sphere_at,
};
use crate::ecs::world::{World, WorldCommand};
use crate::plugin::{EngineCommand, EngineEvent, Primitive};
use crate::render::wgpu::texture_cache::texture_cache_add_reference;
use nalgebra_glm::{Quat, Vec3};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;

use super::async_results::AsyncResult;
use super::entity_mapping::EntityMapping;
use super::events::{PluginInstance, send_engine_event_to_plugin};

pub struct CommandContext<'a> {
    pub plugins: &'a mut [PluginInstance],
    pub plugin_id_to_index: &'a std::collections::HashMap<u64, usize>,
    pub entity_mapping: &'a mut EntityMapping,
    pub texture_id_to_name: &'a mut std::collections::HashMap<u64, String>,
    pub async_sender: &'a Sender<AsyncResult>,
    pub next_texture_id: &'a mut u64,
    pub base_path: PathBuf,
    pub canonical_base_path: PathBuf,
}

pub fn process_engine_commands(
    world: &mut World,
    context: &mut CommandContext<'_>,
    commands: Vec<(u64, EngineCommand)>,
) {
    for (plugin_id, command) in commands {
        match command {
            EngineCommand::Log { message } => {
                tracing::info!("[Plugin {}] {}", plugin_id, message);
            }
            EngineCommand::SpawnPrimitive {
                primitive,
                x,
                y,
                z,
                request_id,
            } => {
                let pos = Vec3::new(x, y, z);
                let entity = match primitive {
                    Primitive::Cube => spawn_cube_at(world, pos),
                    Primitive::Sphere => spawn_sphere_at(world, pos),
                    Primitive::Cylinder => spawn_cylinder_at(world, pos),
                    Primitive::Plane => spawn_plane_at(world, pos),
                    Primitive::Cone => spawn_cone_at(world, pos),
                };
                let entity_id = context.entity_mapping.register(entity);
                let event = EngineEvent::EntitySpawned {
                    request_id,
                    entity_id,
                };
                dispatch_event_to_plugin(
                    context.plugins,
                    context.plugin_id_to_index,
                    plugin_id,
                    &event,
                );
            }
            EngineCommand::DespawnEntity { entity_id } => {
                if let Some(entity) = context.entity_mapping.get(entity_id) {
                    world.queue_command(WorldCommand::DespawnRecursive { entity });
                    context.entity_mapping.unregister(entity_id);
                } else {
                    tracing::warn!(
                        "Plugin {} tried to despawn invalid entity {}",
                        plugin_id,
                        entity_id
                    );
                }
            }
            EngineCommand::SetEntityPosition { entity_id, x, y, z } => {
                if let Some(entity) = context.entity_mapping.get(entity_id) {
                    if let Some(transform) = world.mutate_local_transform(entity) {
                        transform.translation = Vec3::new(x, y, z);
                    }
                    world.set_local_transform_dirty(entity, LocalTransformDirty);
                } else {
                    tracing::warn!(
                        "Plugin {} tried to set position on invalid entity {}",
                        plugin_id,
                        entity_id
                    );
                }
            }
            EngineCommand::SetEntityScale { entity_id, x, y, z } => {
                if let Some(entity) = context.entity_mapping.get(entity_id) {
                    if let Some(transform) = world.mutate_local_transform(entity) {
                        transform.scale = Vec3::new(x, y, z);
                    }
                    world.set_local_transform_dirty(entity, LocalTransformDirty);
                } else {
                    tracing::warn!(
                        "Plugin {} tried to set scale on invalid entity {}",
                        plugin_id,
                        entity_id
                    );
                }
            }
            EngineCommand::SetEntityRotation {
                entity_id,
                x,
                y,
                z,
                w,
            } => {
                if let Some(entity) = context.entity_mapping.get(entity_id) {
                    if let Some(transform) = world.mutate_local_transform(entity) {
                        transform.rotation = Quat::new(w, x, y, z);
                    }
                    world.set_local_transform_dirty(entity, LocalTransformDirty);
                } else {
                    tracing::warn!(
                        "Plugin {} tried to set rotation on invalid entity {}",
                        plugin_id,
                        entity_id
                    );
                }
            }
            EngineCommand::GetEntityPosition {
                entity_id,
                request_id,
            } => {
                let event = match context.entity_mapping.get(entity_id) {
                    Some(entity) => {
                        if let Some(transform) = world.get_local_transform(entity) {
                            EngineEvent::EntityPosition {
                                request_id,
                                entity_id,
                                x: transform.translation.x,
                                y: transform.translation.y,
                                z: transform.translation.z,
                            }
                        } else {
                            EngineEvent::EntityNotFound {
                                request_id,
                                entity_id,
                            }
                        }
                    }
                    None => EngineEvent::EntityNotFound {
                        request_id,
                        entity_id,
                    },
                };
                dispatch_event_to_plugin(
                    context.plugins,
                    context.plugin_id_to_index,
                    plugin_id,
                    &event,
                );
            }
            EngineCommand::GetEntityScale {
                entity_id,
                request_id,
            } => {
                let event = match context.entity_mapping.get(entity_id) {
                    Some(entity) => {
                        if let Some(transform) = world.get_local_transform(entity) {
                            EngineEvent::EntityScale {
                                request_id,
                                entity_id,
                                x: transform.scale.x,
                                y: transform.scale.y,
                                z: transform.scale.z,
                            }
                        } else {
                            EngineEvent::EntityNotFound {
                                request_id,
                                entity_id,
                            }
                        }
                    }
                    None => EngineEvent::EntityNotFound {
                        request_id,
                        entity_id,
                    },
                };
                dispatch_event_to_plugin(
                    context.plugins,
                    context.plugin_id_to_index,
                    plugin_id,
                    &event,
                );
            }
            EngineCommand::GetEntityRotation {
                entity_id,
                request_id,
            } => {
                let event = match context.entity_mapping.get(entity_id) {
                    Some(entity) => {
                        if let Some(transform) = world.get_local_transform(entity) {
                            EngineEvent::EntityRotation {
                                request_id,
                                entity_id,
                                x: transform.rotation.i,
                                y: transform.rotation.j,
                                z: transform.rotation.k,
                                w: transform.rotation.w,
                            }
                        } else {
                            EngineEvent::EntityNotFound {
                                request_id,
                                entity_id,
                            }
                        }
                    }
                    None => EngineEvent::EntityNotFound {
                        request_id,
                        entity_id,
                    },
                };
                dispatch_event_to_plugin(
                    context.plugins,
                    context.plugin_id_to_index,
                    plugin_id,
                    &event,
                );
            }
            EngineCommand::ReadFile { path, request_id } => {
                if let Some(safe_path) =
                    sanitize_path(&context.base_path, &context.canonical_base_path, &path)
                {
                    let sender = context.async_sender.clone();
                    std::thread::spawn(move || {
                        let result = std::fs::read(&safe_path);
                        let _ = sender.send(AsyncResult::File {
                            plugin_id,
                            request_id,
                            result,
                        });
                    });
                } else {
                    tracing::warn!("Plugin {} tried to read invalid path: {}", plugin_id, path);
                    let event = EngineEvent::FileError {
                        request_id,
                        error: "Invalid path: access denied".to_string(),
                    };
                    dispatch_event_to_plugin(
                        context.plugins,
                        context.plugin_id_to_index,
                        plugin_id,
                        &event,
                    );
                }
            }
            EngineCommand::LoadTexture { path, request_id } => {
                if let Some(safe_path) =
                    sanitize_path(&context.base_path, &context.canonical_base_path, &path)
                {
                    let sender = context.async_sender.clone();
                    let texture_name = path.clone();
                    let texture_id = *context.next_texture_id;
                    *context.next_texture_id += 1;
                    std::thread::spawn(move || {
                        let result = std::fs::read(&safe_path)
                            .map_err(|error| error.to_string())
                            .and_then(|data| {
                                image::load_from_memory(&data)
                                    .map_err(|error| format!("Failed to decode image: {}", error))
                                    .map(|img| {
                                        let rgba = img.to_rgba8();
                                        let (width, height) = rgba.dimensions();
                                        (rgba.into_raw(), width, height)
                                    })
                            });
                        let _ = sender.send(AsyncResult::Texture {
                            plugin_id,
                            request_id,
                            texture_name,
                            texture_id,
                            result,
                        });
                    });
                } else {
                    tracing::warn!(
                        "Plugin {} tried to load texture from invalid path: {}",
                        plugin_id,
                        path
                    );
                    let event = EngineEvent::AssetError {
                        request_id,
                        error: "Invalid path: access denied".to_string(),
                    };
                    dispatch_event_to_plugin(
                        context.plugins,
                        context.plugin_id_to_index,
                        plugin_id,
                        &event,
                    );
                }
            }
            EngineCommand::LoadPrefab {
                path,
                x,
                y,
                z,
                request_id,
            } => {
                if let Some(safe_path) =
                    sanitize_path(&context.base_path, &context.canonical_base_path, &path)
                {
                    let sender = context.async_sender.clone();
                    let position = Vec3::new(x, y, z);
                    std::thread::spawn(move || {
                        let result =
                            import_gltf_from_path(&safe_path).map_err(|error| error.to_string());
                        let _ = sender.send(AsyncResult::Prefab {
                            plugin_id,
                            request_id,
                            position,
                            result,
                        });
                    });
                } else {
                    tracing::warn!(
                        "Plugin {} tried to load prefab from invalid path: {}",
                        plugin_id,
                        path
                    );
                    let event = EngineEvent::AssetError {
                        request_id,
                        error: "Invalid path: access denied".to_string(),
                    };
                    dispatch_event_to_plugin(
                        context.plugins,
                        context.plugin_id_to_index,
                        plugin_id,
                        &event,
                    );
                }
            }
            EngineCommand::SetEntityMaterial {
                entity_id,
                texture_id,
            } => {
                if let Some(entity) = context.entity_mapping.get(entity_id) {
                    if let Some(texture_name) = context.texture_id_to_name.get(&texture_id).cloned()
                    {
                        let material_ref = world.get_material_ref(entity).cloned();
                        if let Some(mat_ref) = material_ref {
                            if let Some(current_material) = registry_entry_by_name(
                                &world.resources.material_registry.registry,
                                &mat_ref.name,
                            )
                            .cloned()
                            {
                                let mut new_material = current_material;
                                new_material.base_texture = Some(texture_name.clone());
                                texture_cache_add_reference(
                                    &mut world.resources.texture_cache,
                                    &texture_name,
                                );
                                set_material_with_textures(world, entity, new_material);
                            }
                        } else {
                            tracing::warn!(
                                "Plugin {} tried to set material on entity {} without material",
                                plugin_id,
                                entity_id
                            );
                        }
                    } else {
                        tracing::warn!(
                            "Plugin {} tried to use invalid texture {}",
                            plugin_id,
                            texture_id
                        );
                    }
                } else {
                    tracing::warn!(
                        "Plugin {} tried to set material on invalid entity {}",
                        plugin_id,
                        entity_id
                    );
                }
            }
            EngineCommand::SetEntityColor {
                entity_id,
                r,
                g,
                b,
                a,
            } => {
                if let Some(entity) = context.entity_mapping.get(entity_id) {
                    let material_ref = world.get_material_ref(entity).cloned();
                    if let Some(mat_ref) = material_ref {
                        if let Some(current_material) = registry_entry_by_name(
                            &world.resources.material_registry.registry,
                            &mat_ref.name,
                        )
                        .cloned()
                        {
                            let mut new_material = current_material;
                            new_material.base_color = [r, g, b, a];
                            set_material_with_textures(world, entity, new_material);
                        }
                    } else {
                        tracing::warn!(
                            "Plugin {} tried to set color on entity {} without material",
                            plugin_id,
                            entity_id
                        );
                    }
                } else {
                    tracing::warn!(
                        "Plugin {} tried to set color on invalid entity {}",
                        plugin_id,
                        entity_id
                    );
                }
            }
        }
    }
}

pub fn process_async_results(
    world: &mut World,
    plugins: &mut [PluginInstance],
    plugin_id_to_index: &std::collections::HashMap<u64, usize>,
    entity_mapping: &mut EntityMapping,
    texture_id_to_name: &mut std::collections::HashMap<u64, String>,
    async_receiver: &std::sync::mpsc::Receiver<AsyncResult>,
) {
    while let Ok(result) = async_receiver.try_recv() {
        match result {
            AsyncResult::File {
                plugin_id,
                request_id,
                result,
            } => {
                let event = match result {
                    Ok(data) => EngineEvent::FileLoaded { request_id, data },
                    Err(error) => EngineEvent::FileError {
                        request_id,
                        error: error.to_string(),
                    },
                };
                dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
            }
            AsyncResult::Texture {
                plugin_id,
                request_id,
                texture_name,
                texture_id,
                result,
            } => match result {
                Ok((rgba_data, width, height)) => {
                    world.queue_command(WorldCommand::LoadTexture {
                        name: texture_name.clone(),
                        rgba_data,
                        width,
                        height,
                    });
                    texture_id_to_name.insert(texture_id, texture_name);
                    let event = EngineEvent::TextureLoaded {
                        request_id,
                        texture_id,
                    };
                    dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
                }
                Err(error) => {
                    let event = EngineEvent::AssetError { request_id, error };
                    dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
                }
            },
            AsyncResult::Prefab {
                plugin_id,
                request_id,
                position,
                result,
            } => match result {
                Ok(gltf_result) => {
                    for (mesh_name, mesh) in gltf_result.meshes {
                        mesh_cache_insert(&mut world.resources.mesh_cache, mesh_name, mesh);
                    }
                    for (texture_name, (rgba_data, width, height)) in gltf_result.textures {
                        world.queue_command(WorldCommand::LoadTexture {
                            name: texture_name,
                            rgba_data,
                            width,
                            height,
                        });
                    }
                    if let Some(prefab) = gltf_result.prefabs.first() {
                        let entity = spawn_prefab(world, prefab, position);
                        let entity_id = entity_mapping.register(entity);
                        let event = EngineEvent::PrefabLoaded {
                            request_id,
                            entity_id,
                        };
                        dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
                    } else {
                        let event = EngineEvent::AssetError {
                            request_id,
                            error: "No prefabs found in glTF file".to_string(),
                        };
                        dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
                    }
                }
                Err(error) => {
                    let event = EngineEvent::AssetError { request_id, error };
                    dispatch_event_to_plugin(plugins, plugin_id_to_index, plugin_id, &event);
                }
            },
        }
    }
}

fn dispatch_event_to_plugin(
    plugins: &mut [PluginInstance],
    plugin_id_to_index: &std::collections::HashMap<u64, usize>,
    plugin_id: u64,
    event: &EngineEvent,
) {
    if let Some(&index) = plugin_id_to_index.get(&plugin_id) {
        if let Some(plugin) = plugins.get_mut(index) {
            send_engine_event_to_plugin(plugin, event);
        }
    } else {
        tracing::warn!("Plugin {} no longer exists, dropping event", plugin_id);
    }
}

fn sanitize_path(
    base_path: &Path,
    canonical_base_path: &Path,
    path: &str,
) -> Option<std::path::PathBuf> {
    let path = Path::new(path);

    if path.is_absolute() {
        return None;
    }

    for component in path.components() {
        match component {
            std::path::Component::ParentDir => return None,
            std::path::Component::Prefix(_) => return None,
            std::path::Component::RootDir => return None,
            _ => {}
        }
    }

    let full_path = base_path.join(path);

    let canonical_full = match std::fs::canonicalize(&full_path) {
        Ok(path) => path,
        Err(_) => return None,
    };

    if !canonical_full.starts_with(canonical_base_path) {
        return None;
    }

    Some(canonical_full)
}
