//! Transactional gravity placement using the editor's hydrated Avian world.

use std::collections::HashSet;
use std::time::Duration;

use avian3d::prelude::*;
use bevy::prelude::*;

use crate::history::{set_transform_group_with_history, EditorHistory};
use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus};
use crate::state::EditorMode;
use crate::ui::selection_ops::{entity_name, is_mutable_level_object};
use crate::viewport::collider_diagnostics::physics_placement_prerequisite_problems;

const STEP_HZ: f64 = 120.0;
const STEPS_PER_FRAME: usize = 4;
const SETTLE_LINEAR_SPEED: f32 = 0.04;
const SETTLE_ANGULAR_SPEED: f32 = 0.04;
const SETTLE_STEPS: u32 = 30;
const MAX_STEPS: u32 = 1_200;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PhysicsPlacementPhase {
    #[default]
    Inactive,
    Dropping,
    Settled,
    TimedOut,
}

impl PhysicsPlacementPhase {
    pub fn label(self) -> &'static str {
        match self {
            Self::Inactive => "Inactive",
            Self::Dropping => "Simulating",
            Self::Settled => "Settled",
            Self::TimedOut => "Preview stopped",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PhysicsPlacementRequest {
    #[default]
    None,
    Commit,
    Cancel,
}

#[derive(Component, Debug, Clone, Copy, Default)]
pub struct PhysicsPlacementPreview;

#[derive(Resource, Debug, Default)]
pub struct PhysicsPlacementState {
    pub phase: PhysicsPlacementPhase,
    pub request: PhysicsPlacementRequest,
    pub last_error: Option<String>,
    pub selected_count: usize,
    pub simulated_steps: u32,
    session: Option<PlacementSession>,
}

impl PhysicsPlacementState {
    pub fn active(&self) -> bool {
        self.session.is_some()
    }

    pub fn simulated_seconds(&self) -> f32 {
        self.simulated_steps as f32 / STEP_HZ as f32
    }
}

#[derive(Debug)]
struct PlacementSession {
    selected: Vec<BodySnapshot>,
    frozen: Vec<BodySnapshot>,
    stable_steps: u32,
}

#[derive(Debug, Clone)]
struct BodySnapshot {
    entity: Entity,
    transform: Transform,
    position: Option<Position>,
    rotation: Option<Rotation>,
    rigid_body: RigidBody,
    linear_velocity: Option<LinearVelocity>,
    angular_velocity: Option<AngularVelocity>,
    gravity_scale: Option<GravityScale>,
    sleeping: bool,
    sleeping_disabled: bool,
    rigid_body_disabled: bool,
}

impl BodySnapshot {
    fn capture(world: &World, entity: Entity) -> Result<Self, String> {
        Ok(Self {
            entity,
            transform: world
                .get::<Transform>(entity)
                .copied()
                .ok_or_else(|| format!("{} has no Transform", entity_name(world, entity)))?,
            position: world.get::<Position>(entity).copied(),
            rotation: world.get::<Rotation>(entity).copied(),
            rigid_body: world.get::<RigidBody>(entity).copied().ok_or_else(|| {
                format!("{} has no hydrated rigid body", entity_name(world, entity))
            })?,
            linear_velocity: world.get::<LinearVelocity>(entity).copied(),
            angular_velocity: world.get::<AngularVelocity>(entity).copied(),
            gravity_scale: world.get::<GravityScale>(entity).copied(),
            sleeping: world.get::<Sleeping>(entity).is_some(),
            sleeping_disabled: world.get::<SleepingDisabled>(entity).is_some(),
            rigid_body_disabled: world.get::<RigidBodyDisabled>(entity).is_some(),
        })
    }
}

pub struct PhysicsPlacementPlugin;

impl Plugin for PhysicsPlacementPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<PhysicsPlacementState>()
            .add_systems(PostStartup, pause_physics_for_editor)
            .add_systems(PostUpdate, tick_physics_placement)
            .add_systems(OnExit(EditorMode::Editing), cancel_physics_placement);
    }
}

fn pause_physics_for_editor(mut physics_time: ResMut<Time<Physics>>) {
    physics_time.pause();
}

pub fn begin_physics_placement(
    world: &mut World,
    selected: impl IntoIterator<Item = Entity>,
) -> Result<(), String> {
    if *world.resource::<State<EditorMode>>().get() != EditorMode::Editing {
        return placement_error(world, "Physics placement is available only in Edit mode");
    }
    if world.resource::<PhysicsPlacementState>().active() {
        return placement_error(world, "A physics placement preview is already active");
    }

    let mut unique = HashSet::new();
    let selected = selected
        .into_iter()
        .filter(|entity| unique.insert(*entity))
        .collect::<Vec<_>>();
    validate_selection(world, &selected)?;

    let selected_set = selected.iter().copied().collect::<HashSet<_>>();
    let selected_snapshots = selected
        .iter()
        .map(|entity| BodySnapshot::capture(world, *entity))
        .collect::<Result<Vec<_>, _>>()?;

    let movable = world
        .query::<(Entity, &RigidBody)>()
        .iter(world)
        .filter_map(|(entity, body)| {
            (!selected_set.contains(&entity) && *body != RigidBody::Static).then_some(entity)
        })
        .collect::<Vec<_>>();
    let frozen_snapshots = movable
        .iter()
        .map(|entity| BodySnapshot::capture(world, *entity))
        .collect::<Result<Vec<_>, _>>()?;

    for snapshot in &frozen_snapshots {
        if let Ok(mut entity) = world.get_entity_mut(snapshot.entity) {
            entity.insert(RigidBody::Static);
        }
    }
    for snapshot in &selected_snapshots {
        let global = world
            .get::<GlobalTransform>(snapshot.entity)
            .copied()
            .unwrap_or_else(|| GlobalTransform::from(snapshot.transform));
        if let Ok(mut entity) = world.get_entity_mut(snapshot.entity) {
            entity.insert((
                RigidBody::Dynamic,
                LinearVelocity::ZERO,
                AngularVelocity::ZERO,
                GravityScale(1.0),
                Position(global.translation()),
                Rotation(global.rotation()),
                PhysicsPlacementPreview,
            ));
            entity.remove::<Sleeping>();
            entity.remove::<RigidBodyDisabled>();
        }
    }

    let selected_count = selected_snapshots.len();
    let mut state = world.resource_mut::<PhysicsPlacementState>();
    state.phase = PhysicsPlacementPhase::Dropping;
    state.request = PhysicsPlacementRequest::None;
    state.last_error = None;
    state.selected_count = selected_count;
    state.simulated_steps = 0;
    state.session = Some(PlacementSession {
        selected: selected_snapshots,
        frozen: frozen_snapshots,
        stable_steps: 0,
    });
    set_operator_status(
        world,
        OperatorPhase::Preview,
        format!("Dropping {selected_count} selected prop(s)"),
        Vec::new(),
    );
    Ok(())
}

pub fn request_physics_placement(world: &mut World, request: PhysicsPlacementRequest) {
    if world.resource::<PhysicsPlacementState>().active() {
        world.resource_mut::<PhysicsPlacementState>().request = request;
    }
}

pub fn cancel_physics_placement(world: &mut World) {
    finish_physics_placement(world, false);
}

pub fn commit_physics_placement(world: &mut World) {
    finish_physics_placement(world, true);
}

fn finish_physics_placement(world: &mut World, commit: bool) {
    let Some(mut state) = world.remove_resource::<PhysicsPlacementState>() else {
        return;
    };
    let Some(session) = state.session.take() else {
        world.insert_resource(state);
        return;
    };

    let final_transforms = session
        .selected
        .iter()
        .filter_map(|snapshot| {
            world
                .get::<Transform>(snapshot.entity)
                .copied()
                .map(|transform| (snapshot.entity, transform))
        })
        .collect::<Vec<_>>();

    for snapshot in &session.frozen {
        restore_snapshot(world, snapshot, true);
    }
    for snapshot in &session.selected {
        restore_snapshot(world, snapshot, !commit);
        if let Ok(mut entity) = world.get_entity_mut(snapshot.entity) {
            entity.remove::<PhysicsPlacementPreview>();
        }
    }

    let undo_depth = world.resource::<EditorHistory>().undo_depth();
    if commit {
        let changes = session.selected.iter().filter_map(|snapshot| {
            final_transforms
                .iter()
                .find(|(entity, _)| *entity == snapshot.entity)
                .map(|(_, new)| (snapshot.entity, snapshot.transform, *new))
        });
        set_transform_group_with_history(world, changes);
    }
    let committed = commit && world.resource::<EditorHistory>().undo_depth() > undo_depth;

    state.phase = PhysicsPlacementPhase::Inactive;
    state.request = PhysicsPlacementRequest::None;
    state.selected_count = 0;
    state.simulated_steps = 0;
    state.last_error = None;
    world.insert_resource(state);
    let (phase, hint) = if committed {
        (OperatorPhase::Committed, "Physics placement committed")
    } else if commit {
        (OperatorPhase::Canceled, "Physics placement made no changes")
    } else {
        (OperatorPhase::Canceled, "Physics placement canceled")
    };
    set_operator_status(world, phase, hint.to_string(), Vec::new());
}

fn tick_physics_placement(world: &mut World) {
    let escape = world
        .get_resource::<ButtonInput<KeyCode>>()
        .is_some_and(|keys| keys.just_pressed(KeyCode::Escape));
    let enter = world
        .get_resource::<ButtonInput<KeyCode>>()
        .is_some_and(|keys| keys.just_pressed(KeyCode::Enter));
    let request = world.resource::<PhysicsPlacementState>().request;
    let settled = matches!(
        world.resource::<PhysicsPlacementState>().phase,
        PhysicsPlacementPhase::Settled | PhysicsPlacementPhase::TimedOut
    );
    if escape || request == PhysicsPlacementRequest::Cancel {
        cancel_physics_placement(world);
        return;
    }
    if request == PhysicsPlacementRequest::Commit || (enter && settled) {
        commit_physics_placement(world);
        return;
    }
    if world.resource::<PhysicsPlacementState>().phase != PhysicsPlacementPhase::Dropping {
        return;
    }

    for _ in 0..STEPS_PER_FRAME {
        world
            .resource_mut::<Time<Physics>>()
            .advance_by(Duration::from_secs_f64(1.0 / STEP_HZ));
        world.run_schedule(FixedPostUpdate);
    }

    let Some(mut state) = world.remove_resource::<PhysicsPlacementState>() else {
        return;
    };
    state.simulated_steps = state.simulated_steps.saturating_add(STEPS_PER_FRAME as u32);
    let Some(session) = state.session.as_mut() else {
        world.insert_resource(state);
        return;
    };
    let at_rest = session.selected.iter().all(|snapshot| {
        let linear = world
            .get::<LinearVelocity>(snapshot.entity)
            .map_or(0.0, |velocity| velocity.length());
        let angular = world
            .get::<AngularVelocity>(snapshot.entity)
            .map_or(0.0, |velocity| velocity.length());
        (linear <= SETTLE_LINEAR_SPEED && angular <= SETTLE_ANGULAR_SPEED)
            || world.get::<Sleeping>(snapshot.entity).is_some()
    });
    session.stable_steps = if at_rest {
        session.stable_steps.saturating_add(STEPS_PER_FRAME as u32)
    } else {
        0
    };

    let mut status = None;
    if session.stable_steps >= SETTLE_STEPS {
        state.phase = PhysicsPlacementPhase::Settled;
        status = Some((
            OperatorPhase::Preview,
            "Placement settled; commit or cancel".to_string(),
            Vec::new(),
        ));
    } else if state.simulated_steps >= MAX_STEPS {
        state.phase = PhysicsPlacementPhase::TimedOut;
        status = Some((
            OperatorPhase::Preview,
            "Placement reached its 10 second preview limit".to_string(),
            vec!["Bodies are still moving; commit the current pose or cancel".to_string()],
        ));
    }
    world.insert_resource(state);
    if let Some((phase, hint, warnings)) = status {
        set_operator_status(world, phase, hint, warnings);
    }
}

fn validate_selection(world: &mut World, selected: &[Entity]) -> Result<(), String> {
    if selected.is_empty() {
        return placement_error(world, "Select at least one prop to place with physics");
    }
    let mut problems = Vec::new();
    for entity in selected {
        let name = entity_name(world, *entity);
        if !is_mutable_level_object(world, *entity) {
            problems.push(format!(
                "{name}: selection is locked, linked, or not a level object"
            ));
            continue;
        }
        problems.extend(physics_placement_prerequisite_problems(world, *entity));
    }
    if problems.is_empty() {
        Ok(())
    } else {
        placement_error(world, problems.join("\n"))
    }
}

fn restore_snapshot(world: &mut World, snapshot: &BodySnapshot, restore_pose: bool) {
    let Ok(mut entity) = world.get_entity_mut(snapshot.entity) else {
        return;
    };
    entity.insert(snapshot.rigid_body);
    restore_optional(&mut entity, snapshot.linear_velocity);
    restore_optional(&mut entity, snapshot.angular_velocity);
    restore_optional(&mut entity, snapshot.gravity_scale);
    restore_marker::<Sleeping>(&mut entity, snapshot.sleeping);
    restore_marker::<SleepingDisabled>(&mut entity, snapshot.sleeping_disabled);
    restore_marker::<RigidBodyDisabled>(&mut entity, snapshot.rigid_body_disabled);
    if restore_pose {
        entity.insert(snapshot.transform);
        restore_optional(&mut entity, snapshot.position);
        restore_optional(&mut entity, snapshot.rotation);
    }
}

fn restore_optional<T: Component + Clone>(entity: &mut EntityWorldMut<'_>, value: Option<T>) {
    if let Some(value) = value {
        entity.insert(value);
    } else {
        entity.remove::<T>();
    }
}

fn restore_marker<T: Component + Default>(entity: &mut EntityWorldMut<'_>, present: bool) {
    if present {
        entity.insert(T::default());
    } else {
        entity.remove::<T>();
    }
}

fn placement_error<T>(world: &mut World, message: impl Into<String>) -> Result<T, String> {
    let message = message.into();
    world.resource_mut::<PhysicsPlacementState>().last_error = Some(message.clone());
    set_operator_status(
        world,
        OperatorPhase::Blocked,
        "Physics placement could not start".to_string(),
        message.lines().map(str::to_string).collect(),
    );
    Err(message)
}

fn set_operator_status(
    world: &mut World,
    phase: OperatorPhase,
    hint: String,
    warnings: Vec<String>,
) {
    if let Some(mut operator) = world.get_resource_mut::<ActiveOperator>() {
        operator.status = Some(OperatorStatus {
            id: "physics.placement".to_string(),
            label: "Physics Placement".to_string(),
            phase,
            hint,
            warnings,
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::history::EditorHistory;
    use crate::operators::test_harness::{assert_undo_redo_round_trip, OperatorInvariantHarness};
    use crate::scene_io::SceneIo;
    use bevy::asset::{AssetApp, AssetPlugin};
    use bevy::state::app::StatesPlugin;
    use shared::{ColliderDesc, LevelObject, RigidBodyDesc};

    fn app_with_physics() -> App {
        let mut app = App::new();
        app.add_plugins((
            MinimalPlugins,
            AssetPlugin::default(),
            StatesPlugin,
            TransformPlugin,
            PhysicsPlugins::default(),
        ))
        .init_asset::<Mesh>()
        .init_state::<EditorMode>()
        .init_resource::<PhysicsPlacementState>()
        .init_resource::<EditorHistory>()
        .init_resource::<ActiveOperator>()
        .init_resource::<SceneIo>();
        app.finish();
        app.cleanup();
        app.update();
        app.world_mut().resource_mut::<Time<Physics>>().pause();
        app
    }

    fn spawn_placeable(world: &mut World, name: &str, transform: Transform) -> Entity {
        world
            .spawn((
                Name::new(name.to_string()),
                LevelObject,
                RigidBodyDesc::default(),
                ColliderDesc::default(),
                RigidBody::Static,
                Collider::cuboid(1.0, 1.0, 1.0),
                transform,
            ))
            .id()
    }

    #[test]
    fn reports_missing_authoring_prerequisites_without_starting() {
        let mut app = app_with_physics();
        let entity = app
            .world_mut()
            .spawn((Name::new("No Physics"), LevelObject, Transform::default()))
            .id();
        let harness = OperatorInvariantHarness::capture(app.world_mut());

        let error = begin_physics_placement(app.world_mut(), [entity]).unwrap_err();

        assert!(error.contains("add and enable a Rigid Body"));
        assert!(error.contains("add and enable a non-trigger Collider"));
        assert!(!app.world().resource::<PhysicsPlacementState>().active());
        harness.assert_blocked(app.world_mut());
        harness.assert_status(app.world(), "physics.placement", OperatorPhase::Blocked);
        harness.assert_no_helpers::<PhysicsPlacementPreview>(app.world_mut());
    }

    #[test]
    fn cancel_restores_selected_and_unrelated_motion_state_exactly() {
        let mut app = app_with_physics();
        let selected = spawn_placeable(
            app.world_mut(),
            "Selected",
            Transform::from_xyz(1.0, 4.0, 2.0),
        );
        app.world_mut().entity_mut(selected).insert((
            RigidBody::Kinematic,
            LinearVelocity(Vec3::new(1.0, 2.0, 3.0)),
            AngularVelocity(Vec3::new(0.1, 0.2, 0.3)),
            GravityScale(0.25),
            SleepingDisabled,
        ));
        let unrelated = app
            .world_mut()
            .spawn((
                RigidBody::Dynamic,
                Collider::sphere(0.5),
                LinearVelocity(Vec3::X * 3.0),
                Transform::from_xyz(8.0, 2.0, 0.0),
            ))
            .id();
        app.update();
        let harness = OperatorInvariantHarness::capture(app.world_mut());

        begin_physics_placement(app.world_mut(), [selected]).unwrap();
        assert_eq!(
            app.world().get::<RigidBody>(selected),
            Some(&RigidBody::Dynamic)
        );
        assert_eq!(
            app.world().get::<RigidBody>(unrelated),
            Some(&RigidBody::Static)
        );
        for _ in 0..10 {
            tick_physics_placement(app.world_mut());
        }
        assert!(
            app.world()
                .get::<Transform>(selected)
                .unwrap()
                .translation
                .y
                < 4.0
        );

        cancel_physics_placement(app.world_mut());

        assert_eq!(
            app.world().get::<Transform>(selected).unwrap().translation,
            Vec3::new(1.0, 4.0, 2.0)
        );
        assert_eq!(
            app.world().get::<RigidBody>(selected),
            Some(&RigidBody::Kinematic)
        );
        assert_eq!(
            app.world().get::<LinearVelocity>(selected),
            Some(&LinearVelocity(Vec3::new(1.0, 2.0, 3.0)))
        );
        assert_eq!(
            app.world().get::<GravityScale>(selected),
            Some(&GravityScale(0.25))
        );
        assert!(app.world().get::<SleepingDisabled>(selected).is_some());
        assert_eq!(
            app.world().get::<RigidBody>(unrelated),
            Some(&RigidBody::Dynamic)
        );
        assert_eq!(
            app.world().get::<LinearVelocity>(unrelated),
            Some(&LinearVelocity(Vec3::X * 3.0))
        );
        assert_eq!(app.world().resource::<EditorHistory>().undo_depth(), 0);
        harness.assert_canceled(app.world_mut());
        harness.assert_status(app.world(), "physics.placement", OperatorPhase::Canceled);
        harness.assert_no_helpers::<PhysicsPlacementPreview>(app.world_mut());
    }

    #[test]
    fn real_physics_drop_commits_once_and_undo_restores_start_transform() {
        let mut app = app_with_physics();
        app.world_mut().spawn((
            RigidBody::Static,
            Collider::cuboid(20.0, 0.5, 20.0),
            Transform::from_xyz(0.0, -0.25, 0.0),
        ));
        let start = Transform::from_xyz(0.0, 4.0, 0.0);
        let prop = spawn_placeable(app.world_mut(), "Drop Prop", start);
        app.update();
        let harness = OperatorInvariantHarness::capture(app.world_mut());

        begin_physics_placement(app.world_mut(), [prop]).unwrap();
        for _ in 0..400 {
            tick_physics_placement(app.world_mut());
            if app.world().resource::<PhysicsPlacementState>().phase
                == PhysicsPlacementPhase::Settled
            {
                break;
            }
        }
        assert_eq!(
            app.world().resource::<PhysicsPlacementState>().phase,
            PhysicsPlacementPhase::Settled
        );
        let settled = *app.world().get::<Transform>(prop).unwrap();
        assert!(settled.translation.y < 1.0, "{settled:?}");

        commit_physics_placement(app.world_mut());

        harness.assert_committed(app.world_mut(), 1, 0);
        harness.assert_status(app.world(), "physics.placement", OperatorPhase::Committed);
        harness.assert_no_helpers::<PhysicsPlacementPreview>(app.world_mut());
        assert_eq!(
            app.world().resource::<EditorHistory>().status,
            "Undo: Move Selection"
        );
        assert_eq!(app.world().get::<RigidBody>(prop), Some(&RigidBody::Static));
        assert_eq!(*app.world().get::<Transform>(prop).unwrap(), settled);
        assert_undo_redo_round_trip(app.world_mut(), start, settled, |world| {
            *world.get::<Transform>(prop).unwrap()
        });
    }

    #[test]
    fn no_op_commit_terminates_without_dirtying_or_history() {
        let mut app = app_with_physics();
        let start = Transform::from_xyz(0.0, 4.0, 0.0);
        let prop = spawn_placeable(app.world_mut(), "Static Prop", start);
        app.update();
        let harness = OperatorInvariantHarness::capture(app.world_mut());

        begin_physics_placement(app.world_mut(), [prop]).unwrap();
        commit_physics_placement(app.world_mut());

        harness.assert_canceled(app.world_mut());
        harness.assert_status(app.world(), "physics.placement", OperatorPhase::Canceled);
        harness.assert_no_helpers::<PhysicsPlacementPreview>(app.world_mut());
        assert_eq!(*app.world().get::<Transform>(prop).unwrap(), start);
        assert_eq!(app.world().get::<RigidBody>(prop), Some(&RigidBody::Static));
    }

    #[test]
    fn multi_select_commit_is_one_transaction_and_cleans_every_preview_helper() {
        let mut app = app_with_physics();
        let starts = [
            Transform::from_xyz(-1.0, 4.0, 0.0),
            Transform::from_xyz(1.0, 6.0, 0.0),
        ];
        let first = spawn_placeable(app.world_mut(), "First", starts[0]);
        let second = spawn_placeable(app.world_mut(), "Second", starts[1]);
        app.update();
        let harness = OperatorInvariantHarness::capture(app.world_mut());

        begin_physics_placement(app.world_mut(), [first, second, first]).unwrap();
        assert_eq!(
            app.world()
                .resource::<PhysicsPlacementState>()
                .selected_count,
            2,
            "duplicate selections must collapse to one placement snapshot"
        );
        let committed = [
            Transform::from_xyz(-1.0, 0.5, 0.0),
            Transform::from_xyz(1.0, 0.5, 0.0),
        ];
        app.world_mut().entity_mut(first).insert(committed[0]);
        app.world_mut().entity_mut(second).insert(committed[1]);

        commit_physics_placement(app.world_mut());

        harness.assert_committed(app.world_mut(), 1, 0);
        harness.assert_status(app.world(), "physics.placement", OperatorPhase::Committed);
        harness.assert_no_helpers::<PhysicsPlacementPreview>(app.world_mut());
        assert_undo_redo_round_trip(app.world_mut(), starts, committed, |world| {
            [
                *world.get::<Transform>(first).unwrap(),
                *world.get::<Transform>(second).unwrap(),
            ]
        });
    }
}
