//! Shared lifecycle for editor actions that mutate or drive tools.

use bevy::prelude::*;

use crate::history::EditorHistory;

#[cfg(test)]
pub(crate) mod test_harness;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OperatorPhase {
    Idle,
    Preview,
    Committed,
    Canceled,
    Blocked,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OperatorStatus {
    pub id: String,
    pub label: String,
    pub phase: OperatorPhase,
    pub hint: String,
    pub warnings: Vec<String>,
}

impl OperatorStatus {
    fn new(id: &str, label: &str, phase: OperatorPhase, hint: impl Into<String>) -> Self {
        Self {
            id: id.to_string(),
            label: label.to_string(),
            phase,
            hint: hint.into(),
            warnings: Vec::new(),
        }
    }
}

#[derive(Resource, Debug, Default)]
pub struct ActiveOperator {
    pub status: Option<OperatorStatus>,
}

impl ActiveOperator {
    pub fn label(&self) -> Option<String> {
        self.status
            .as_ref()
            .map(|status| match status.hint.is_empty() {
                true => format!("Tool: {}", status.label),
                false => format!("Tool: {} - {}", status.label, status.hint),
            })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OperatorAvailability {
    Ready,
    Disabled(String),
}

pub trait EditorOperator {
    fn id(&self) -> &str;
    fn label(&self) -> &str;

    fn can_start(&self, _world: &World) -> OperatorAvailability {
        OperatorAvailability::Ready
    }

    fn begin(&mut self, _world: &mut World) -> Result<(), String> {
        Ok(())
    }

    fn preview(&mut self, _world: &mut World) -> Result<(), String> {
        Ok(())
    }

    fn commit(&mut self, world: &mut World) -> Result<(), String>;

    fn cancel(&mut self, _world: &mut World) {}

    fn undo_label(&self) -> String {
        self.label().to_string()
    }
}

pub struct OperatorPlugin;

impl Plugin for OperatorPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<ActiveOperator>();
    }
}

pub fn run_operator(world: &mut World, operator: &mut dyn EditorOperator) -> bool {
    let undo_depth_before = world
        .get_resource::<EditorHistory>()
        .map(EditorHistory::undo_depth)
        .unwrap_or_default();

    match operator.can_start(world) {
        OperatorAvailability::Ready => {}
        OperatorAvailability::Disabled(reason) => {
            set_operator_status(
                world,
                OperatorStatus::new(
                    operator.id(),
                    operator.label(),
                    OperatorPhase::Blocked,
                    reason,
                ),
            );
            return false;
        }
    }

    set_operator_status(
        world,
        OperatorStatus::new(
            operator.id(),
            operator.label(),
            OperatorPhase::Preview,
            "Preview",
        ),
    );

    if let Err(err) = operator.begin(world).and_then(|_| operator.preview(world)) {
        operator.cancel(world);
        set_operator_status(
            world,
            OperatorStatus::new(
                operator.id(),
                operator.label(),
                OperatorPhase::Canceled,
                err,
            ),
        );
        return false;
    }

    match operator.commit(world) {
        Ok(()) => {
            let status = OperatorStatus::new(
                operator.id(),
                operator.label(),
                OperatorPhase::Committed,
                format!("Committed {}", operator.undo_label()),
            );
            set_operator_status(world, status);
            let undo_depth_after = world
                .get_resource::<EditorHistory>()
                .map(EditorHistory::undo_depth)
                .unwrap_or_default();
            if undo_depth_after > undo_depth_before {
                let Some(label) = world.resource::<ActiveOperator>().label() else {
                    return true;
                };
                world.resource_mut::<EditorHistory>().set_undo_status(label);
            }
            true
        }
        Err(err) => {
            operator.cancel(world);
            set_operator_status(
                world,
                OperatorStatus::new(
                    operator.id(),
                    operator.label(),
                    OperatorPhase::Canceled,
                    err,
                ),
            );
            false
        }
    }
}

pub fn run_immediate_operator(
    world: &mut World,
    id: &str,
    label: &str,
    availability: OperatorAvailability,
    commit: impl FnOnce(&mut World) -> Result<(), String>,
) -> bool {
    struct ImmediateOperator<F> {
        id: String,
        label: String,
        availability: OperatorAvailability,
        commit: Option<F>,
    }

    impl<F> EditorOperator for ImmediateOperator<F>
    where
        F: FnOnce(&mut World) -> Result<(), String>,
    {
        fn id(&self) -> &str {
            &self.id
        }

        fn label(&self) -> &str {
            &self.label
        }

        fn can_start(&self, _world: &World) -> OperatorAvailability {
            self.availability.clone()
        }

        fn commit(&mut self, world: &mut World) -> Result<(), String> {
            self.commit
                .take()
                .expect("immediate operator commit must run once")(world)
        }
    }

    let mut operator = ImmediateOperator {
        id: id.to_string(),
        label: label.to_string(),
        availability,
        commit: Some(commit),
    };
    run_operator(world, &mut operator)
}

fn set_operator_status(world: &mut World, status: OperatorStatus) {
    if let Some(mut active) = world.get_resource_mut::<ActiveOperator>() {
        active.status = Some(status);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::history::EditorHistory;
    use crate::operators::test_harness::OperatorInvariantHarness;
    use crate::scene_io::SceneIo;

    #[derive(Resource, Default)]
    struct Counter(i32);

    #[derive(Component)]
    struct PreviewHelper;

    struct CounterOperator {
        before: i32,
        helper: Option<Entity>,
    }

    impl EditorOperator for CounterOperator {
        fn id(&self) -> &str {
            "test.counter"
        }

        fn label(&self) -> &str {
            "Increment Counter"
        }

        fn begin(&mut self, world: &mut World) -> Result<(), String> {
            self.before = world.resource::<Counter>().0;
            self.helper = Some(world.spawn(PreviewHelper).id());
            Ok(())
        }

        fn preview(&mut self, world: &mut World) -> Result<(), String> {
            world.resource_mut::<Counter>().0 += 1;
            Ok(())
        }

        fn commit(&mut self, world: &mut World) -> Result<(), String> {
            self.remove_helper(world);
            Ok(())
        }

        fn cancel(&mut self, world: &mut World) {
            world.resource_mut::<Counter>().0 = self.before;
            self.remove_helper(world);
        }
    }

    impl CounterOperator {
        fn remove_helper(&mut self, world: &mut World) {
            if let Some(helper) = self.helper.take() {
                if let Ok(entity) = world.get_entity_mut(helper) {
                    entity.despawn();
                }
            }
        }
    }

    fn test_world() -> World {
        let mut world = World::new();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<Counter>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        world
    }

    #[test]
    fn operator_commit_keeps_previewed_change() {
        let mut world = test_world();
        let harness = OperatorInvariantHarness::capture(&mut world);

        let mut operator = CounterOperator {
            before: 0,
            helper: None,
        };

        assert!(run_operator(&mut world, &mut operator));
        assert_eq!(world.resource::<Counter>().0, 1);
        harness.assert_committed(&mut world, 0, 0);
        harness.assert_no_helpers::<PreviewHelper>(&mut world);
    }

    #[test]
    fn operator_cancel_restores_previewed_change_on_commit_error() {
        struct FailingCounterOperator(CounterOperator);

        impl EditorOperator for FailingCounterOperator {
            fn id(&self) -> &str {
                self.0.id()
            }

            fn label(&self) -> &str {
                self.0.label()
            }

            fn begin(&mut self, world: &mut World) -> Result<(), String> {
                self.0.begin(world)
            }

            fn preview(&mut self, world: &mut World) -> Result<(), String> {
                self.0.preview(world)
            }

            fn commit(&mut self, _world: &mut World) -> Result<(), String> {
                Err("commit failed".to_string())
            }

            fn cancel(&mut self, world: &mut World) {
                self.0.cancel(world);
            }
        }

        let mut world = test_world();
        let harness = OperatorInvariantHarness::capture(&mut world);

        let mut operator = FailingCounterOperator(CounterOperator {
            before: 0,
            helper: None,
        });

        assert!(!run_operator(&mut world, &mut operator));
        assert_eq!(world.resource::<Counter>().0, 0);
        harness.assert_canceled(&mut world);
        harness.assert_no_helpers::<PreviewHelper>(&mut world);
    }

    #[test]
    fn blocked_operator_never_begins_or_mutates_world() {
        let mut world = test_world();
        let harness = OperatorInvariantHarness::capture(&mut world);

        let result = run_immediate_operator(
            &mut world,
            "test.blocked",
            "Blocked Operator",
            OperatorAvailability::Disabled("fixture unavailable".to_string()),
            |world| {
                world.resource_mut::<Counter>().0 += 1;
                Ok(())
            },
        );

        assert!(!result);
        assert_eq!(world.resource::<Counter>().0, 0);
        harness.assert_blocked(&mut world);
    }
}
