//! Editor-visible component registry for actor inspector palettes.

use std::collections::{HashMap, HashSet};

use bevy::ecs::reflect::ReflectComponent;
use bevy::prelude::*;
use bevy::reflect::std_traits::ReflectDefault;
use bevy::reflect::GetTypeRegistration;
use bevy_egui::egui;
use egui_phosphor_icons::icons;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditorComponentCategory {
    Authoring,
    Rendering,
    Animation,
    Audio,
    Navigation,
    Physics,
    Gameplay,
    Volumes,
    Editor,
}

#[derive(Debug, Clone)]
pub struct EditorComponentDescriptor {
    /// Stable editor/protocol identity. This must not be a Rust type path.
    pub id: &'static str,
    pub type_name: &'static str,
    pub display_name: &'static str,
    pub category: EditorComponentCategory,
    pub addable: bool,
    pub removable: bool,
    pub reorderable: bool,
    pub hidden: bool,
    pub icon: &'static str,
    pub description: &'static str,
    pub search_terms: &'static [&'static str],
    pub recommended: &'static [&'static str],
    pub conflicts_with: &'static [&'static str],
    pub hydration_effect: &'static str,
}

#[derive(Resource, Debug, Clone)]
pub struct EditorComponentRegistry {
    pub descriptors: Vec<EditorComponentDescriptor>,
    inspectors: HashMap<&'static str, ComponentInspectorFn>,
}

pub type ComponentInspectorFn = fn(&mut World, &mut egui::Ui, Entity);

impl EditorComponentRegistry {
    /// Registers a statically linked component extension.
    ///
    /// Stable ids and reflected type paths are both unique. Returning an error
    /// here prevents a game/editor extension from silently shadowing a built-in.
    pub fn register(&mut self, descriptor: EditorComponentDescriptor) -> Result<(), String> {
        if self
            .descriptors
            .iter()
            .any(|existing| existing.id == descriptor.id)
        {
            return Err(format!(
                "duplicate authoring component id `{}`",
                descriptor.id
            ));
        }
        if self
            .descriptors
            .iter()
            .any(|existing| existing.type_name == descriptor.type_name)
        {
            return Err(format!(
                "component type `{}` is already registered",
                descriptor.type_name
            ));
        }
        self.descriptors.push(descriptor);
        Ok(())
    }

    pub fn register_with_inspector(
        &mut self,
        descriptor: EditorComponentDescriptor,
        inspector: ComponentInspectorFn,
    ) -> Result<(), String> {
        let type_name = descriptor.type_name;
        self.register(descriptor)?;
        self.inspectors.insert(type_name, inspector);
        Ok(())
    }

    pub fn inspector(&self, type_name: &str) -> Option<ComponentInspectorFn> {
        self.inspectors.get(type_name).copied()
    }

    /// Hard lifecycle requirements keyed by stable component IDs. Keeping this
    /// lookup in the registry removes requirement policy from inspector code.
    pub fn required_component_ids(
        &self,
        descriptor: &EditorComponentDescriptor,
    ) -> &'static [&'static str] {
        match descriptor.id {
            shared::AUTHORING_COMPONENT_ANIMATION_CONTROLLER => {
                &[shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER]
            }
            _ => &[],
        }
    }

    pub fn present_dependents(
        &self,
        world: &World,
        entity: Entity,
        required_id: &str,
    ) -> Vec<&EditorComponentDescriptor> {
        self.descriptors
            .iter()
            .filter(|descriptor| {
                self.required_component_ids(descriptor)
                    .contains(&required_id)
                    && self.component_present(world, entity, descriptor.type_name)
            })
            .collect()
    }

    pub fn by_id(&self, id: &str) -> Option<&EditorComponentDescriptor> {
        self.descriptors
            .iter()
            .find(|descriptor| descriptor.id == id)
    }

    pub fn by_type_name(&self, type_name: &str) -> Option<&EditorComponentDescriptor> {
        self.descriptors
            .iter()
            .find(|descriptor| descriptor.type_name == type_name)
    }

    pub fn stable_id_for_type<'a>(&self, type_name: &'a str) -> &'a str {
        self.by_type_name(type_name)
            .map(|descriptor| descriptor.id)
            .unwrap_or(type_name)
    }

    pub fn ensure_component_order(&self, order: &mut shared::InspectorOrder, present: &[&str]) {
        for type_name in present {
            let component_id = self.stable_id_for_type(type_name);
            if !order
                .component_ids
                .iter()
                .any(|existing| existing == component_id)
            {
                order.component_ids.push(component_id.to_string());
            }
        }
        order.component_type_names.clear();
    }

    pub fn ordered_components<'a>(
        &self,
        order: &shared::InspectorOrder,
        present: &[&'a str],
    ) -> Vec<&'a str> {
        let mut ordered = Vec::new();
        for component_id in &order.component_ids {
            if let Some(type_name) = present
                .iter()
                .find(|type_name| self.stable_id_for_type(type_name) == component_id)
            {
                ordered.push(*type_name);
            }
        }
        for legacy_type_name in &order.component_type_names {
            if let Some(type_name) = present
                .iter()
                .find(|type_name| **type_name == legacy_type_name)
            {
                if !ordered.contains(type_name) {
                    ordered.push(*type_name);
                }
            }
        }
        for type_name in present {
            if !ordered.contains(type_name) {
                ordered.push(*type_name);
            }
        }
        ordered
    }

    pub fn move_component(
        &self,
        order: &mut shared::InspectorOrder,
        type_name: &str,
        offset: isize,
        present: &[&str],
    ) -> bool {
        self.ensure_component_order(order, present);
        let component_id = self.stable_id_for_type(type_name);
        let Some(index) = order
            .component_ids
            .iter()
            .position(|existing| existing == component_id)
        else {
            return false;
        };
        let target =
            (index as isize + offset).clamp(0, order.component_ids.len() as isize - 1) as usize;
        if index == target {
            return false;
        }
        let item = order.component_ids.remove(index);
        order.component_ids.insert(target, item);
        true
    }

    pub fn component_present(&self, world: &World, entity: Entity, type_name: &str) -> bool {
        let Ok(entity_ref) = world.get_entity(entity) else {
            return false;
        };
        let Some(app_registry) = world.get_resource::<AppTypeRegistry>() else {
            return false;
        };
        let type_registry = app_registry.read();
        type_registry
            .get_with_type_path(type_name)
            .and_then(|registration| registration.data::<ReflectComponent>())
            .is_some_and(|component| component.contains(entity_ref))
    }

    /// Validates registry metadata against Bevy reflection before the first
    /// inspector frame. This is also the contract test used by extensions.
    pub fn validate(&self, world: &World) -> Result<(), Vec<String>> {
        let Some(app_registry) = world.get_resource::<AppTypeRegistry>() else {
            return Err(vec!["AppTypeRegistry is unavailable".to_string()]);
        };
        let type_registry = app_registry.read();
        let mut errors = Vec::new();
        let mut ids = HashSet::new();
        let mut type_names = HashSet::new();
        for descriptor in &self.descriptors {
            if descriptor.id.trim().is_empty() {
                errors.push(format!(
                    "{} has an empty stable id",
                    descriptor.display_name
                ));
            } else if !ids.insert(descriptor.id) {
                errors.push(format!("duplicate stable id `{}`", descriptor.id));
            }
            if !type_names.insert(descriptor.type_name) {
                errors.push(format!(
                    "duplicate reflected type `{}`",
                    descriptor.type_name
                ));
            }
            match type_registry.get_with_type_path(descriptor.type_name) {
                Some(registration) if registration.data::<ReflectComponent>().is_none() => errors
                    .push(format!(
                        "`{}` is registered without ReflectComponent",
                        descriptor.type_name
                    )),
                None => errors.push(format!(
                    "reflected component `{}` is not registered",
                    descriptor.type_name
                )),
                Some(registration)
                    if descriptor.addable && registration.data::<ReflectDefault>().is_none() =>
                {
                    errors.push(format!(
                        "addable component `{}` has no reflected Default",
                        descriptor.type_name
                    ));
                }
                Some(_) => {}
            }
            for relation in descriptor
                .recommended
                .iter()
                .chain(descriptor.conflicts_with.iter())
            {
                if !self
                    .descriptors
                    .iter()
                    .any(|candidate| candidate.type_name == *relation)
                {
                    errors.push(format!(
                        "{} references unregistered component `{relation}`",
                        descriptor.id
                    ));
                }
            }
            for required_id in self.required_component_ids(descriptor) {
                if self.by_id(required_id).is_none() {
                    errors.push(format!(
                        "{} requires unknown stable component id `{required_id}`",
                        descriptor.id
                    ));
                }
            }
        }
        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}

impl Default for EditorComponentRegistry {
    fn default() -> Self {
        Self {
            descriptors: vec![
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_STATIC_MESH_RENDERER,
                    type_name: "shared::components::StaticMeshRenderer",
                    display_name: "Static Mesh Renderer",
                    category: EditorComponentCategory::Rendering,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::CUBE.as_str(),
                    description: "Renders one or more normalized imported mesh slots.",
                    search_terms: &["mesh", "renderer", "rendering", "model", "static"],
                    recommended: &[],
                    conflicts_with: &[
                        "shared::components::Primitive",
                        "shared::components::BrushDesc",
                        "shared::animation::SkinnedMeshRenderer",
                    ],
                    hydration_effect:
                        "Hydrates into generated mesh children and material bindings.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER,
                    type_name: "shared::animation::SkinnedMeshRenderer",
                    display_name: "Skinned Mesh Renderer",
                    category: EditorComponentCategory::Rendering,
                    addable: false,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::PERSON_SIMPLE_RUN.as_str(),
                    description:
                        "Renders rigged geometry while preserving its imported joints and skin bindings.",
                    search_terms: &["mesh", "renderer", "skinned", "skeletal", "rigged", "joints"],
                    recommended: &["shared::animation::AnimationControllerDesc"],
                    conflicts_with: &[
                        "shared::components::Primitive",
                        "shared::components::BrushDesc",
                        "shared::components::StaticMeshRenderer",
                    ],
                    hydration_effect:
                        "Hydrates through a dedicated scene root containing Bevy skinned meshes and joints.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_BRUSH,
                    type_name: "shared::components::BrushDesc",
                    display_name: "Brush",
                    category: EditorComponentCategory::Authoring,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::CUBE.as_str(),
                    description: "Creates authored convex brush geometry for blockout.",
                    search_terms: &["brush", "blockout", "convex", "csg", "geometry"],
                    recommended: &["shared::components::MaterialDesc"],
                    conflicts_with: &[
                        "shared::components::Primitive",
                        "shared::components::StaticMeshRenderer",
                        "shared::animation::SkinnedMeshRenderer",
                    ],
                    hydration_effect: "Hydrates into generated brush mesh children.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_MATERIAL,
                    type_name: "shared::components::MaterialDesc",
                    display_name: "Authoring Material",
                    category: EditorComponentCategory::Rendering,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::PALETTE.as_str(),
                    description: "Defines an authored material directly on this actor.",
                    search_terms: &["material", "shader", "texture"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates into Bevy material assets for renderable actors.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_RIGID_BODY,
                    type_name: "shared::components::RigidBodyDesc",
                    display_name: "Rigid Body",
                    category: EditorComponentCategory::Physics,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::SPHERE.as_str(),
                    description: "Controls authored rigid-body behavior for physics hydration.",
                    search_terms: &["body", "physics", "rigidbody"],
                    recommended: &["shared::components::ColliderDesc"],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates into runtime Avian rigid-body state.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_COLLIDER,
                    type_name: "shared::components::ColliderDesc",
                    display_name: "Collider",
                    category: EditorComponentCategory::Physics,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::SELECTION.as_str(),
                    description: "Defines authored collision shape data.",
                    search_terms: &["collision", "trigger", "physics"],
                    recommended: &["shared::components::RigidBodyDesc"],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates into runtime Avian collider state.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_LIGHT,
                    type_name: "shared::components::LightDesc",
                    display_name: "Light",
                    category: EditorComponentCategory::Rendering,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::LIGHTBULB.as_str(),
                    description: "Adds authored point, spot, directional, or area lighting.",
                    search_terms: &["light", "lamp", "illumination", "sun"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates into runtime Bevy light components.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_ANIMATION_CONTROLLER,
                    type_name: "shared::animation::AnimationControllerDesc",
                    display_name: "Animation Controller",
                    category: EditorComponentCategory::Animation,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::FILM_SLATE.as_str(),
                    description: "Authors named animation states for an imported model rig.",
                    search_terms: &["animation", "controller", "state", "clip", "skeleton", "rig"],
                    recommended: &["shared::animation::SkinnedMeshRenderer"],
                    conflicts_with: &[],
                    hydration_effect:
                        "Hydrates into a Bevy animation graph and player bound to the imported model.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_AUDIO_SOURCE,
                    type_name: "shared::components::AudioSourceDesc",
                    display_name: "Audio Source",
                    category: EditorComponentCategory::Audio,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::SPEAKER_HIGH.as_str(),
                    description: "Plays a routed 2D or spatial audio clip from this actor.",
                    search_terms: &["audio", "sound", "clip", "speaker", "spatial"],
                    recommended: &[],
                    conflicts_with: &["shared::components::AudioListenerDesc"],
                    hydration_effect:
                        "Hydrates into runtime Bevy audio voices with bus and attenuation control.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_AUDIO_LISTENER,
                    type_name: "shared::components::AudioListenerDesc",
                    display_name: "Audio Listener",
                    category: EditorComponentCategory::Audio,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::HEADPHONES.as_str(),
                    description: "Receives spatial audio at this actor transform.",
                    search_terms: &["audio", "sound", "listener", "ears", "spatial"],
                    recommended: &[],
                    conflicts_with: &["shared::components::AudioSourceDesc"],
                    hydration_effect:
                        "The highest-priority enabled listener becomes the runtime listener.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_PRIMITIVE,
                    type_name: "shared::components::Primitive",
                    display_name: "Primitive",
                    category: EditorComponentCategory::Authoring,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::CUBE.as_str(),
                    description: "Creates an authored built-in primitive shape.",
                    search_terms: &["box", "sphere", "ramp", "primitive"],
                    recommended: &[],
                    conflicts_with: &[
                        "shared::components::StaticMeshRenderer",
                        "shared::components::BrushDesc",
                        "shared::animation::SkinnedMeshRenderer",
                    ],
                    hydration_effect:
                        "Hydrates into generated primitive render and collision data.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_PLAYER_SPAWN,
                    type_name: "shared::components::PlayerSpawn",
                    display_name: "Player Spawn",
                    category: EditorComponentCategory::Gameplay,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::PERSON_SIMPLE_RUN.as_str(),
                    description: "Marks where Play mode should spawn the player.",
                    search_terms: &["player", "spawn", "start"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect:
                        "Used by PIE/session startup; no saved runtime player is created.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_WEAPON_SPAWN,
                    type_name: "shared::components::WeaponSpawn",
                    display_name: "Weapon Spawn",
                    category: EditorComponentCategory::Gameplay,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::CROSSHAIR.as_str(),
                    description: "Marks a gameplay weapon spawn point.",
                    search_terms: &["weapon", "spawn"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates into gameplay spawn metadata.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_TRIGGER_VOLUME,
                    type_name: "shared::components::TriggerVolume",
                    display_name: "Trigger Volume",
                    category: EditorComponentCategory::Volumes,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::SELECTION.as_str(),
                    description: "Adds an authored gameplay trigger volume.",
                    search_terms: &["trigger", "volume", "event"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates into gameplay trigger queries and visualizers.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_TEAM_SPAWN,
                    type_name: "shared::components::TeamSpawn",
                    display_name: "Team Spawn",
                    category: EditorComponentCategory::Gameplay,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::FLAG.as_str(),
                    description: "Marks a gameplay team spawn point.",
                    search_terms: &["team", "spawn"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates into gameplay spawn metadata.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_OBJECTIVE,
                    type_name: "shared::components::ObjectiveMarker",
                    display_name: "Objective Marker",
                    category: EditorComponentCategory::Gameplay,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::FLAG.as_str(),
                    description: "Marks an authored gameplay objective location.",
                    search_terms: &["objective", "marker", "gameplay"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates into gameplay objective metadata.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_POST_PROCESS_VOLUME,
                    type_name: "shared::components::PostProcessVolumeDesc",
                    display_name: "Post-Process Volume",
                    category: EditorComponentCategory::Volumes,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::APERTURE.as_str(),
                    description: "Adds a local rendering profile override volume.",
                    search_terms: &["post", "process", "volume", "rendering", "fx"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect:
                        "Affects active camera rendering when the camera is inside the volume.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_NAVIGATION_BOUNDS,
                    type_name: "shared::navigation::NavigationBounds",
                    display_name: "Navigation Bounds",
                    category: EditorComponentCategory::Navigation,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::PATH.as_str(),
                    description: "Defines the world region and agent profile for a navigation bake.",
                    search_terms: &["navigation", "navmesh", "bounds", "agent", "bake"],
                    recommended: &[],
                    conflicts_with: &[
                        "shared::navigation::NavigationObstacle",
                        "shared::navigation::NavigationArea",
                        "shared::navigation::NavigationLink",
                    ],
                    hydration_effect: "Feeds the generated navigation artifact; no render component is hydrated.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_NAVIGATION_OBSTACLE,
                    type_name: "shared::navigation::NavigationObstacle",
                    display_name: "Navigation Obstacle",
                    category: EditorComponentCategory::Navigation,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::SELECTION_SLASH.as_str(),
                    description: "Carves an axis-aligned blocked volume from overlapping bakes.",
                    search_terms: &["navigation", "navmesh", "obstacle", "block", "carve"],
                    recommended: &[],
                    conflicts_with: &[
                        "shared::navigation::NavigationBounds",
                        "shared::navigation::NavigationArea",
                        "shared::navigation::NavigationLink",
                    ],
                    hydration_effect: "Contributes a deterministic unwalkable volume during bake.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_NAVIGATION_AREA,
                    type_name: "shared::navigation::NavigationArea",
                    display_name: "Navigation Area",
                    category: EditorComponentCategory::Navigation,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::POLYGON.as_str(),
                    description: "Labels a walkable or blocked navigation area and traversal cost.",
                    search_terms: &["navigation", "navmesh", "area", "cost", "walkable"],
                    recommended: &[],
                    conflicts_with: &[
                        "shared::navigation::NavigationBounds",
                        "shared::navigation::NavigationObstacle",
                        "shared::navigation::NavigationLink",
                    ],
                    hydration_effect: "Stores area metadata and blocked-volume contribution in the bake artifact.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_NAVIGATION_LINK,
                    type_name: "shared::navigation::NavigationLink",
                    display_name: "Navigation Link",
                    category: EditorComponentCategory::Navigation,
                    addable: true,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::LINK.as_str(),
                    description: "Connects otherwise isolated navigation regions with an explicit traversal.",
                    search_terms: &["navigation", "navmesh", "link", "jump", "door", "ladder"],
                    recommended: &[],
                    conflicts_with: &[
                        "shared::navigation::NavigationBounds",
                        "shared::navigation::NavigationObstacle",
                        "shared::navigation::NavigationArea",
                    ],
                    hydration_effect: "Adds a validated directed or bidirectional runtime path connection.",
                },
                EditorComponentDescriptor {
                    id: shared::AUTHORING_COMPONENT_LEGACY_PHYSICS_BODY,
                    type_name: "shared::components::PhysicsBody",
                    display_name: "Legacy Physics Body",
                    category: EditorComponentCategory::Physics,
                    addable: false,
                    removable: true,
                    reorderable: true,
                    hidden: false,
                    icon: icons::SPHERE.as_str(),
                    description: "Legacy combined physics authoring retained for scene compatibility.",
                    search_terms: &["legacy", "physics", "body"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates through the compatibility physics path.",
                },
                EditorComponentDescriptor {
                    id: "prefab.instance",
                    type_name: "shared::components::PrefabInstance",
                    display_name: "Prefab Instance",
                    category: EditorComponentCategory::Authoring,
                    addable: false,
                    removable: false,
                    reorderable: true,
                    hidden: false,
                    icon: icons::PACKAGE.as_str(),
                    description: "Stores a linked prefab instance and its overrides.",
                    search_terms: &["prefab", "instance", "link"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates linked prefab members beneath this anchor.",
                },
                EditorComponentDescriptor {
                    id: "prefab.reference",
                    type_name: "shared::components::PrefabRef",
                    display_name: "Prefab Reference",
                    category: EditorComponentCategory::Authoring,
                    addable: false,
                    removable: false,
                    reorderable: false,
                    hidden: true,
                    icon: icons::PACKAGE.as_str(),
                    description: "Stable source reference owned by a prefab anchor.",
                    search_terms: &["prefab", "reference"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Resolves the prefab source asset.",
                },
                EditorComponentDescriptor {
                    id: "render.imported_model",
                    type_name: "shared::components::ModelRef",
                    display_name: "Imported Model Reference",
                    category: EditorComponentCategory::Rendering,
                    addable: false,
                    removable: false,
                    reorderable: false,
                    hidden: true,
                    icon: icons::CUBE_TRANSPARENT.as_str(),
                    description: "Legacy full-scene imported model reference.",
                    search_terms: &["model", "imported", "scene"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Hydrates the referenced imported model scene.",
                },
                EditorComponentDescriptor {
                    id: "render.material_override_legacy",
                    type_name: "shared::components::MaterialOverride",
                    display_name: "Legacy Material Override",
                    category: EditorComponentCategory::Rendering,
                    addable: false,
                    removable: false,
                    reorderable: false,
                    hidden: true,
                    icon: icons::PALETTE.as_str(),
                    description: "Legacy renderer material slot overrides retained for migration.",
                    search_terms: &["material", "override", "legacy"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Applies compatibility material overrides.",
                },
                EditorComponentDescriptor {
                    id: "render.project_sun",
                    type_name: "shared::components::ProjectSun",
                    display_name: "Project Sun",
                    category: EditorComponentCategory::Rendering,
                    addable: false,
                    removable: false,
                    reorderable: true,
                    hidden: false,
                    icon: icons::SUN.as_str(),
                    description: "Marks the scene light that overrides the project sun.",
                    search_terms: &["sun", "directional", "project"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Selects the authored directional project light.",
                },
                EditorComponentDescriptor {
                    id: "core.actor_kind",
                    type_name: "shared::components::ActorKind",
                    display_name: "Actor Kind Hint",
                    category: EditorComponentCategory::Editor,
                    addable: false,
                    removable: false,
                    reorderable: false,
                    hidden: true,
                    icon: icons::TAG.as_str(),
                    description: "Compatibility and presentation hint derived from primary components.",
                    search_terms: &["actor", "kind", "hint"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "No runtime effect; validation uses component composition.",
                },
                EditorComponentDescriptor {
                    id: "editor.component_states",
                    type_name: "shared::components::AuthoringComponentStates",
                    display_name: "Authoring Component States",
                    category: EditorComponentCategory::Editor,
                    addable: false,
                    removable: false,
                    reorderable: false,
                    hidden: true,
                    icon: icons::TOGGLE_LEFT.as_str(),
                    description: "Stores persisted component enable state independently of inspector layout.",
                    search_terms: &["editor", "active", "enabled"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Controls whether opted-in authoring components hydrate.",
                },
                EditorComponentDescriptor {
                    id: "editor.inspector_order",
                    type_name: "shared::components::InspectorOrder",
                    display_name: "Inspector Order",
                    category: EditorComponentCategory::Editor,
                    addable: false,
                    removable: false,
                    reorderable: false,
                    hidden: true,
                    icon: icons::LIST_BULLETS.as_str(),
                    description: "Stores editor-only component card ordering.",
                    search_terms: &["editor", "order"],
                    recommended: &[],
                    conflicts_with: &[],
                    hydration_effect: "Controls editor presentation only; runtime active state is stored separately.",
                },
            ],
            inspectors: HashMap::new(),
        }
    }
}

/// Registers one statically linked game/editor component with reflection,
/// lifecycle metadata, and its editor inspector in a single call.
pub fn register_authoring_component<T>(
    app: &mut App,
    descriptor: EditorComponentDescriptor,
    inspector: ComponentInspectorFn,
) -> Result<(), String>
where
    T: Component + Reflect + GetTypeRegistration,
{
    app.register_type::<T>();
    if !app.world().contains_resource::<EditorComponentRegistry>() {
        app.init_resource::<EditorComponentRegistry>();
    }
    app.world_mut()
        .resource_mut::<EditorComponentRegistry>()
        .register_with_inspector(descriptor, inspector)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn built_in_registry_is_unique_and_reflectable() {
        let mut app = App::new();
        app.add_plugins(shared::SharedTypesPlugin)
            .init_resource::<EditorComponentRegistry>();

        app.world()
            .resource::<EditorComponentRegistry>()
            .validate(app.world())
            .unwrap();
    }

    #[test]
    fn static_extensions_cannot_shadow_stable_ids_or_types() {
        let mut registry = EditorComponentRegistry::default();
        let mut duplicate = registry.descriptors[0].clone();
        duplicate.type_name = "game::editor_ext::StaticFixture";
        assert!(registry.register(duplicate).is_err());

        let mut duplicate = registry.descriptors[0].clone();
        duplicate.id = "game.static_fixture";
        assert!(registry.register(duplicate).is_err());
    }
}
