//! Shared authored/runtime collider diagnostics for editor workflows.

use std::collections::HashMap;

use avian3d::prelude::{Collider, RigidBody};
use bevy::prelude::*;
use shared::{
    authoring_component_active, AuthoringComponentStates, ColliderDesc, ColliderShapeDesc,
    InspectorOrder, LevelObject, PhysicsBody, RigidBodyDesc, COMPONENT_COLLIDER_DESC,
    COMPONENT_PHYSICS_BODY, COMPONENT_RIGID_BODY_DESC,
};

use crate::assets::asset_db::{find_asset_by_id, AssetRegistry};
use crate::ui::selection_ops::entity_name;

/// Collider dimensions beyond this world-space extent are usually accidental in a meter-scale
/// project and can severely degrade broad-phase performance.
pub const OVERSIZED_COLLIDER_EXTENT: f32 = 1_000.0;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ColliderDiagnosticSeverity {
    Info,
    Warning,
    Error,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColliderDiagnostic {
    pub code: &'static str,
    pub severity: ColliderDiagnosticSeverity,
    pub message: String,
    pub repair: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColliderOverlayStatus {
    Valid,
    Trigger,
    Disabled,
    Warning,
    Error,
}

#[derive(Debug, Clone)]
pub struct ColliderDiagnosticEntry {
    pub entity: Entity,
    pub actor_name: String,
    pub shape_label: &'static str,
    pub runtime_ready: bool,
    pub diagnostics: Vec<ColliderDiagnostic>,
    pub overlay_status: ColliderOverlayStatus,
}

impl ColliderDiagnosticEntry {
    pub fn highest_severity(&self) -> Option<ColliderDiagnosticSeverity> {
        self.diagnostics
            .iter()
            .map(|diagnostic| diagnostic.severity)
            .max()
    }

    pub fn blocking_messages(&self) -> impl Iterator<Item = &str> {
        self.diagnostics
            .iter()
            .filter(|diagnostic| diagnostic.severity == ColliderDiagnosticSeverity::Error)
            .map(|diagnostic| diagnostic.message.as_str())
    }
}

#[derive(Resource, Debug, Default)]
pub struct ColliderDiagnosticsState {
    pub entries: HashMap<Entity, ColliderDiagnosticEntry>,
    pub valid_actors: usize,
    pub disabled_actors: usize,
    pub warning_actors: usize,
    pub error_actors: usize,
}

impl ColliderDiagnosticsState {
    pub fn entry(&self, entity: Entity) -> Option<&ColliderDiagnosticEntry> {
        self.entries.get(&entity)
    }
}

pub fn refresh_collider_diagnostics(world: &mut World) {
    let entities = world
        .iter_entities()
        .filter(|entity| entity.contains::<LevelObject>())
        .map(|entity| entity.id())
        .collect::<Vec<_>>();
    let mut state = ColliderDiagnosticsState::default();
    for entity in entities {
        let Some(entry) = diagnose_collider(world, entity) else {
            continue;
        };
        match entry.overlay_status {
            ColliderOverlayStatus::Error => state.error_actors += 1,
            ColliderOverlayStatus::Warning => state.warning_actors += 1,
            ColliderOverlayStatus::Disabled => state.disabled_actors += 1,
            _ => state.valid_actors += 1,
        }
        state.entries.insert(entity, entry);
    }
    world.insert_resource(state);
}

pub fn diagnose_collider(world: &World, entity: Entity) -> Option<ColliderDiagnosticEntry> {
    let states = world.get::<AuthoringComponentStates>(entity);
    let order = world.get::<InspectorOrder>(entity);
    let rigid_body_active = world.get::<RigidBodyDesc>(entity).is_some()
        && authoring_component_active(states, order, COMPONENT_RIGID_BODY_DESC);
    let legacy_active = world.get::<PhysicsBody>(entity).is_some()
        && authoring_component_active(states, order, COMPONENT_PHYSICS_BODY);
    let collider = world.get::<ColliderDesc>(entity);
    let collider_active =
        collider.is_some() && authoring_component_active(states, order, COMPONENT_COLLIDER_DESC);

    if !rigid_body_active && !legacy_active && collider.is_none() {
        return None;
    }

    let mut diagnostics = Vec::new();
    if rigid_body_active && !legacy_active && !collider_active {
        diagnostics.push(ColliderDiagnostic {
            code: "collider.missing",
            severity: ColliderDiagnosticSeverity::Error,
            message: "Rigid body has no active Collider component.".into(),
            repair: "Add or enable a Collider component on this actor.".into(),
        });
    }

    let mut shape_label = if legacy_active { "Legacy" } else { "Missing" };
    let mut enabled = legacy_active;
    let mut trigger = false;
    let mut authoring_valid = true;
    if let Some(collider) = collider {
        shape_label = collider_shape_label(&collider.shape);
        enabled = collider_active && collider.enabled;
        trigger = collider.is_trigger;
        if !collider_active {
            diagnostics.push(ColliderDiagnostic {
                code: "collider.inactive",
                severity: ColliderDiagnosticSeverity::Info,
                message: "Collider component is inactive.".into(),
                repair: "Enable the component when this actor should participate in collision."
                    .into(),
            });
        } else if !collider.enabled {
            diagnostics.push(ColliderDiagnostic {
                code: "collider.disabled",
                severity: ColliderDiagnosticSeverity::Info,
                message: "Collider is disabled.".into(),
                repair: "Enable the collider when collision is required.".into(),
            });
        } else {
            let shape_diagnostics = validate_shape(world, entity, &collider.shape);
            authoring_valid = shape_diagnostics
                .iter()
                .all(|diagnostic| diagnostic.severity != ColliderDiagnosticSeverity::Error);
            diagnostics.extend(shape_diagnostics);
        }
    }

    let runtime_ready = has_runtime_collider(world, entity);
    if enabled && authoring_valid && !runtime_ready {
        diagnostics.push(ColliderDiagnostic {
            code: "collider.runtime_stale",
            severity: ColliderDiagnosticSeverity::Warning,
            message: "Authored collider has not hydrated into a runtime shape.".into(),
            repair: "Check mesh cooking and asset references, then refresh the scene.".into(),
        });
    }

    let highest = diagnostics
        .iter()
        .map(|diagnostic| diagnostic.severity)
        .max();
    let overlay_status = match highest {
        Some(ColliderDiagnosticSeverity::Error) => ColliderOverlayStatus::Error,
        Some(ColliderDiagnosticSeverity::Warning) => ColliderOverlayStatus::Warning,
        _ if !enabled => ColliderOverlayStatus::Disabled,
        _ if trigger => ColliderOverlayStatus::Trigger,
        _ => ColliderOverlayStatus::Valid,
    };

    Some(ColliderDiagnosticEntry {
        entity,
        actor_name: entity_name(world, entity),
        shape_label,
        runtime_ready,
        diagnostics,
        overlay_status,
    })
}

pub fn physics_placement_prerequisite_problems(world: &World, entity: Entity) -> Vec<String> {
    let name = entity_name(world, entity);
    let states = world.get::<AuthoringComponentStates>(entity);
    let order = world.get::<InspectorOrder>(entity);
    let split_body = world.get::<RigidBodyDesc>(entity).is_some()
        && authoring_component_active(states, order, COMPONENT_RIGID_BODY_DESC);
    let legacy = world.get::<PhysicsBody>(entity).is_some()
        && authoring_component_active(states, order, COMPONENT_PHYSICS_BODY);
    let collider = world.get::<ColliderDesc>(entity).filter(|collider| {
        collider.enabled
            && !collider.is_trigger
            && authoring_component_active(states, order, COMPONENT_COLLIDER_DESC)
    });

    let mut problems = Vec::new();
    if !split_body && !legacy {
        problems.push(format!("{name}: add and enable a Rigid Body component"));
    }
    if collider.is_none() && !legacy {
        problems.push(format!(
            "{name}: add and enable a non-trigger Collider component"
        ));
    }
    if let Some(entry) = diagnose_collider(world, entity) {
        problems.extend(
            entry
                .diagnostics
                .iter()
                .filter(|diagnostic| {
                    diagnostic.severity == ColliderDiagnosticSeverity::Error
                        && diagnostic.code != "collider.missing"
                })
                .map(|diagnostic| format!("{name}: {}", diagnostic.message)),
        );
    }
    if world.get::<RigidBody>(entity).is_none() {
        problems.push(format!("{name}: rigid body has not hydrated yet"));
    }
    if !has_runtime_collider(world, entity) {
        problems.push(format!(
            "{name}: collider has not hydrated or finished cooking"
        ));
    }
    problems.sort();
    problems.dedup();
    problems
}

pub fn has_runtime_collider(world: &World, root: Entity) -> bool {
    let mut pending = vec![root];
    while let Some(entity) = pending.pop() {
        if world.get::<Collider>(entity).is_some() {
            return true;
        }
        if let Some(children) = world.get::<Children>(entity) {
            pending.extend(children.iter());
        }
    }
    false
}

pub fn collider_shape_label(shape: &ColliderShapeDesc) -> &'static str {
    match shape {
        ColliderShapeDesc::Cuboid { .. } => "Box",
        ColliderShapeDesc::Sphere { .. } => "Sphere",
        ColliderShapeDesc::Capsule { .. } => "Capsule",
        ColliderShapeDesc::StaticMesh { .. } => "Static Mesh",
    }
}

fn validate_shape(
    world: &World,
    entity: Entity,
    shape: &ColliderShapeDesc,
) -> Vec<ColliderDiagnostic> {
    let mut diagnostics = Vec::new();
    let dimensions = match shape {
        ColliderShapeDesc::Cuboid {
            x_length,
            y_length,
            z_length,
        } => Some(Vec3::new(*x_length, *y_length, *z_length)),
        ColliderShapeDesc::Sphere { radius } => Some(Vec3::splat(*radius * 2.0)),
        ColliderShapeDesc::Capsule { radius, height } => {
            Some(Vec3::new(*radius * 2.0, *height, *radius * 2.0))
        }
        ColliderShapeDesc::StaticMesh { meshes, .. } => {
            if meshes.is_empty() {
                diagnostics.push(ColliderDiagnostic {
                    code: "collider.mesh_missing",
                    severity: ColliderDiagnosticSeverity::Error,
                    message: "Static mesh collider has no mesh sources.".into(),
                    repair: "Choose a mesh source or use the actor renderer meshes.".into(),
                });
            }
            for mesh in meshes {
                if !mesh.is_resolved() {
                    diagnostics.push(ColliderDiagnostic {
                        code: "collider.mesh_invalid_ref",
                        severity: ColliderDiagnosticSeverity::Error,
                        message: format!(
                            "Static mesh source `{}` has no stable asset or sub-asset ID.",
                            mesh.label
                        ),
                        repair: "Choose a valid mesh from the asset browser.".into(),
                    });
                } else if world
                    .get_resource::<AssetRegistry>()
                    .is_some_and(|registry| find_asset_by_id(registry, &mesh.asset_id).is_none())
                {
                    diagnostics.push(ColliderDiagnostic {
                        code: "collider.mesh_stale_ref",
                        severity: ColliderDiagnosticSeverity::Error,
                        message: format!(
                            "Static mesh source `{}` no longer exists in the asset registry.",
                            mesh.label
                        ),
                        repair: "Relink the collider to an existing mesh asset.".into(),
                    });
                }
            }
            None
        }
    };

    if let Some(dimensions) = dimensions {
        if !dimensions.is_finite() || dimensions.min_element() <= 0.0 {
            diagnostics.push(ColliderDiagnostic {
                code: "collider.invalid_dimensions",
                severity: ColliderDiagnosticSeverity::Error,
                message: "Collider dimensions must be positive finite values.".into(),
                repair: "Reset the shape or enter valid dimensions.".into(),
            });
        } else {
            let scale = world
                .get::<GlobalTransform>(entity)
                .map(|global| global.to_scale_rotation_translation().0.abs())
                .unwrap_or(Vec3::ONE);
            let world_extent = (dimensions * scale).max_element();
            if world_extent > OVERSIZED_COLLIDER_EXTENT {
                diagnostics.push(ColliderDiagnostic {
                    code: "collider.oversized",
                    severity: ColliderDiagnosticSeverity::Warning,
                    message: format!(
                        "Collider spans {world_extent:.1} m in world space (recommended maximum: {OVERSIZED_COLLIDER_EXTENT:.0} m)."
                    ),
                    repair: "Reduce collider dimensions or actor scale; split large collision into smaller actors."
                        .into(),
                });
            }
        }
    }
    diagnostics
}

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

    #[test]
    fn reports_missing_invalid_oversized_and_stale_runtime_colliders() {
        let mut world = World::new();
        let missing = world
            .spawn((LevelObject, RigidBodyDesc::default(), Transform::default()))
            .id();
        let invalid = world
            .spawn((
                LevelObject,
                ColliderDesc {
                    enabled: true,
                    is_trigger: false,
                    shape: ColliderShapeDesc::Sphere { radius: 0.0 },
                },
                Transform::default(),
            ))
            .id();
        let oversized = world
            .spawn((
                LevelObject,
                ColliderDesc::static_cuboid(Vec3::splat(2.0)),
                Transform::from_scale(Vec3::splat(600.0)),
                GlobalTransform::from(Transform::from_scale(Vec3::splat(600.0))),
            ))
            .id();

        let missing = diagnose_collider(&world, missing).unwrap();
        assert!(missing
            .diagnostics
            .iter()
            .any(|diagnostic| diagnostic.code == "collider.missing"));

        let invalid = diagnose_collider(&world, invalid).unwrap();
        assert!(invalid
            .diagnostics
            .iter()
            .any(|diagnostic| diagnostic.code == "collider.invalid_dimensions"));

        let oversized = diagnose_collider(&world, oversized).unwrap();
        assert!(oversized
            .diagnostics
            .iter()
            .any(|diagnostic| diagnostic.code == "collider.oversized"));
        assert!(oversized
            .diagnostics
            .iter()
            .any(|diagnostic| diagnostic.code == "collider.runtime_stale"));
    }

    #[test]
    fn hydrated_trigger_has_trigger_overlay_status() {
        let mut world = World::new();
        let entity = world
            .spawn((
                LevelObject,
                RigidBodyDesc {
                    body: AuthoringRigidBody::Static,
                },
                ColliderDesc {
                    enabled: true,
                    is_trigger: true,
                    shape: ColliderShapeDesc::Sphere { radius: 1.0 },
                },
                Collider::sphere(1.0),
                Transform::default(),
            ))
            .id();

        let entry = diagnose_collider(&world, entity).unwrap();
        assert_eq!(entry.overlay_status, ColliderOverlayStatus::Trigger);
        assert!(entry.runtime_ready);
    }
}
