use crate::model::cell::CellValue;
use crate::ui::app::AppMode;
use crate::ui::effect::{self, Effect};
use crate::view::AxisEntry;

use super::core::{Cmd, CmdContext};

#[cfg(test)]
mod tests {
    use super::*;
    use crate::command::cmd::test_helpers::*;

    #[test]
    fn toggle_group_under_cursor_returns_empty_without_groups() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = ToggleGroupAtCursor { is_row: true };
        let effects = cmd.execute(&ctx);
        assert!(effects.is_empty());
    }

    #[test]
    fn law_toggle_group_involution() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = ToggleGroupAtCursor { is_row: true };
        let first = effects_debug(&cmd.execute(&ctx));
        let second = effects_debug(&cmd.execute(&ctx));
        assert_eq!(first, second, "Toggle should be structurally consistent");
    }

    #[test]
    fn view_forward_with_empty_stack_shows_status() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = ViewNavigate { forward: true };
        let effects = cmd.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("No forward view"),
            "Expected status message, got: {dbg}"
        );
    }

    #[test]
    fn view_back_with_empty_stack_shows_status() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = ViewNavigate { forward: false };
        let effects = cmd.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("No previous view"),
            "Expected status message, got: {dbg}"
        );
    }

    #[test]
    fn view_forward_with_stack_produces_effect() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let fwd_stack = vec![crate::ui::app::ViewFrame {
            view_name: "View 2".to_string(),
            mode: crate::ui::app::AppMode::Normal,
        }];
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.view_forward_stack = &fwd_stack;
        let cmd = ViewNavigate { forward: true };
        let effects = cmd.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("ViewForward"),
            "Expected ViewForward, got: {dbg}"
        );
    }

    #[test]
    fn view_back_with_stack_produces_apply_and_back() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let back_stack = vec![crate::ui::app::ViewFrame {
            view_name: "Default".to_string(),
            mode: crate::ui::app::AppMode::Normal,
        }];
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.view_back_stack = &back_stack;
        let cmd = ViewNavigate { forward: false };
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 2);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("ApplyAndClearDrill"),
            "Expected ApplyAndClearDrill, got: {dbg}"
        );
        assert!(dbg.contains("ViewBack"), "Expected ViewBack, got: {dbg}");
    }

    #[test]
    fn toggle_prune_empty_produces_toggle_and_dirty() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = TogglePruneEmpty.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("TogglePruneEmpty"),
            "Expected TogglePruneEmpty, got: {dbg}"
        );
    }

    /// Drilling into a formula cell (e.g. Profit = Revenue - Cost) should
    /// return the underlying data records, not an empty result set. The
    /// formula target coordinate is stripped from the drill key so that
    /// matching_cells finds the raw data backing the formula.
    #[test]
    fn drill_into_formula_cell_returns_data_records() {
        use crate::formula::parse_formula;
        use crate::model::cell::{CellKey, CellValue};
        use crate::workbook::Workbook;

        let mut m = Workbook::new("Test");
        m.add_category("Region").unwrap();
        m.model.category_mut("Region").unwrap().add_item("East");
        m.model
            .category_mut("_Measure")
            .unwrap()
            .add_item("Revenue");
        m.model.category_mut("_Measure").unwrap().add_item("Cost");
        m.model.set_cell(
            CellKey::new(vec![
                ("_Measure".into(), "Revenue".into()),
                ("Region".into(), "East".into()),
            ]),
            CellValue::Number(1000.0),
        );
        m.model.set_cell(
            CellKey::new(vec![
                ("_Measure".into(), "Cost".into()),
                ("Region".into(), "East".into()),
            ]),
            CellValue::Number(600.0),
        );
        m.model
            .add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());

        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);

        // Drill into the Profit/East cell — a formula-derived cell
        let key = CellKey::new(vec![
            ("_Measure".into(), "Profit".into()),
            ("Region".into(), "East".into()),
        ]);
        let cmd = DrillIntoCell { key };
        let effects = cmd.execute(&ctx);
        let dbg = effects_debug(&effects);

        // Should find underlying data records, not "0 rows"
        assert!(
            !dbg.contains("0 rows"),
            "Drill into formula cell should find data records, got: {dbg}"
        );
    }
}

// ── Grid operations ─────────────────────────────────────────────────────

/// Toggle the row or column group collapse under the cursor.
#[derive(Debug)]
pub struct ToggleGroupAtCursor {
    pub is_row: bool,
}
impl Cmd for ToggleGroupAtCursor {
    fn name(&self) -> &'static str {
        if self.is_row {
            "toggle-group-under-cursor"
        } else {
            "toggle-col-group-under-cursor"
        }
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let lookup = if self.is_row {
            ctx.layout.row_group_for(ctx.selected.0)
        } else {
            ctx.layout.col_group_for(ctx.selected.1)
        };
        let Some((cat, group)) = lookup else {
            return vec![];
        };
        vec![
            Box::new(effect::ToggleGroup {
                category: cat,
                group,
            }),
            effect::mark_dirty(),
        ]
    }
}

/// Hide the row item at the cursor.
#[derive(Debug)]
pub struct HideSelectedRowItem;
impl Cmd for HideSelectedRowItem {
    fn name(&self) -> &'static str {
        "hide-selected-row-item"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let Some(cat_name) = ctx.layout.row_cats.first().cloned() else {
            return vec![];
        };
        let sel_row = ctx.selected.0;
        let Some(items) = ctx
            .layout
            .row_items
            .iter()
            .filter_map(|e| {
                if let AxisEntry::DataItem(v) = e {
                    Some(v)
                } else {
                    None
                }
            })
            .nth(sel_row)
        else {
            return vec![];
        };
        let item_name = items[0].clone();
        vec![
            Box::new(effect::HideItem {
                category: cat_name,
                item: item_name,
            }),
            effect::mark_dirty(),
        ]
    }
}

/// Navigate back or forward in view history.
#[derive(Debug)]
pub struct ViewNavigate {
    pub forward: bool,
}
impl Cmd for ViewNavigate {
    fn name(&self) -> &'static str {
        if self.forward {
            "view-forward"
        } else {
            "view-back"
        }
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        if self.forward {
            if ctx.view_forward_stack.is_empty() {
                vec![effect::set_status("No forward view")]
            } else {
                vec![Box::new(effect::ViewForward)]
            }
        } else {
            if ctx.view_back_stack.is_empty() {
                vec![effect::set_status("No previous view")]
            } else {
                vec![
                    Box::new(effect::ApplyAndClearDrill),
                    Box::new(effect::ViewBack),
                ]
            }
        }
    }
}

/// Drill down into an aggregated cell: create a _Drill view with _Index on
/// Row and _Dim on Column (records/long-format view). Fixed coordinates
/// from the drilled cell become page filters.
#[derive(Debug)]
pub struct DrillIntoCell {
    pub key: crate::model::cell::CellKey,
}
impl Cmd for DrillIntoCell {
    fn name(&self) -> &'static str {
        "drill-into-cell"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let drill_name = "_Drill".to_string();
        let mut effects: Vec<Box<dyn Effect>> = Vec::new();

        // If drilling into a formula cell, strip the formula target from the
        // key so matching_cells finds the underlying raw data records instead
        // of returning nothing.
        let drill_key = if let Some(measure_val) = self.key.get("_Measure") {
            let is_formula_target = ctx
                .model
                .formulas()
                .iter()
                .any(|f| f.target_category == "_Measure" && f.target == measure_val);
            if is_formula_target {
                self.key.without("_Measure")
            } else {
                self.key.clone()
            }
        } else {
            self.key.clone()
        };

        // Capture the records snapshot NOW (before we switch views).
        let records: Vec<(crate::model::cell::CellKey, crate::model::cell::CellValue)> =
            if drill_key.0.is_empty() {
                ctx.model
                    .data
                    .iter_cells()
                    .map(|(k, v)| (k, v.clone()))
                    .collect()
            } else {
                ctx.model
                    .data
                    .matching_cells(&drill_key.0)
                    .into_iter()
                    .map(|(k, v)| (k, v.clone()))
                    .collect()
            };
        let n = records.len();

        // Freeze the snapshot in the drill state
        effects.push(Box::new(effect::StartDrill(records)));

        // Create (or replace) the drill view
        effects.push(Box::new(effect::CreateView(drill_name.clone())));
        effects.push(Box::new(effect::SwitchView(drill_name)));

        // Records mode: _Index on Row, _Dim on Column
        effects.push(Box::new(effect::SetAxis {
            category: "_Index".to_string(),
            axis: crate::view::Axis::Row,
        }));
        effects.push(Box::new(effect::SetAxis {
            category: "_Dim".to_string(),
            axis: crate::view::Axis::Column,
        }));

        // Fixed coords (from drilled cell) -> Page with that value as filter
        let fixed_cats: std::collections::HashSet<String> =
            self.key.0.iter().map(|(c, _)| c.clone()).collect();
        for (cat, item) in &self.key.0 {
            effects.push(Box::new(effect::SetAxis {
                category: cat.clone(),
                axis: crate::view::Axis::Page,
            }));
            effects.push(Box::new(effect::SetPageSelection {
                category: cat.clone(),
                item: item.clone(),
            }));
        }
        // Previously-aggregated categories (none_cats) stay on Axis::None so
        // they don't filter records; they'll appear as columns in records mode.
        // Skip virtual categories — we already set _Index/_Dim above.
        for cat in ctx.none_cats() {
            if fixed_cats.contains(cat) || cat.starts_with('_') {
                continue;
            }
            effects.push(Box::new(effect::SetAxis {
                category: cat.clone(),
                axis: crate::view::Axis::None,
            }));
        }

        effects.push(effect::set_status(format!("Drilled into cell: {n} rows")));
        effects
    }
}

/// Toggle pruning of empty rows/columns in the current view.
#[derive(Debug)]
pub struct TogglePruneEmpty;
impl Cmd for TogglePruneEmpty {
    fn name(&self) -> &'static str {
        "toggle-prune-empty"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let currently_on = ctx.view.prune_empty;
        vec![
            Box::new(effect::TogglePruneEmpty),
            effect::set_status(if currently_on {
                "Showing all rows/columns"
            } else {
                "Hiding empty rows/columns"
            }),
        ]
    }
}

/// Toggle between records mode and pivot mode using the view stack.
/// Entering records mode creates a `_Records` view and switches to it.
/// Leaving records mode navigates back to the previous view.
#[derive(Debug)]
pub struct ToggleRecordsMode;
impl Cmd for ToggleRecordsMode {
    fn name(&self) -> &'static str {
        "toggle-records-mode"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let is_records = ctx.layout.is_records_mode();

        if is_records {
            // Leaving records mode: clean up any records with empty CellKeys
            // (produced by AddRecordRow when no page filters are set) before
            // restoring the previous view. This is the inverse of `SortData`
            // that runs on entry.
            return vec![
                Box::new(effect::CleanEmptyRecords),
                Box::new(effect::ViewBack),
                effect::set_status("Pivot mode"),
            ];
        }

        let mut effects: Vec<Box<dyn Effect>> = Vec::new();
        let records_name = "_Records".to_string();

        effects.push(Box::new(effect::SortData));

        // Create (or replace) a _Records view and switch to it
        effects.push(Box::new(effect::CreateView(records_name.clone())));
        effects.push(Box::new(effect::SwitchView(records_name)));

        // _Index on Row, _Dim on Column, everything else -> None
        effects.push(Box::new(effect::SetAxis {
            category: "_Index".to_string(),
            axis: crate::view::Axis::Row,
        }));
        effects.push(Box::new(effect::SetAxis {
            category: "_Dim".to_string(),
            axis: crate::view::Axis::Column,
        }));
        for name in ctx.model.categories.keys() {
            if name != "_Index" && name != "_Dim" {
                effects.push(Box::new(effect::SetAxis {
                    category: name.clone(),
                    axis: crate::view::Axis::None,
                }));
            }
        }
        effects.push(effect::change_mode(AppMode::RecordsNormal));
        effects.push(effect::set_status("Records mode"));
        effects
    }
}

/// In records mode, add a new row with an empty value. The new cell gets
/// coords from the current page filters. In pivot mode, this is a no-op.
#[derive(Debug)]
pub struct AddRecordRow;
impl Cmd for AddRecordRow {
    fn name(&self) -> &'static str {
        "add-record-row"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        if !ctx.is_records_mode() {
            return vec![effect::set_status(
                "add-record-row only works in records mode",
            )];
        }
        // Build a CellKey from the current page filters
        let view = ctx.view;
        let page_cats: Vec<String> = view
            .categories_on(crate::view::Axis::Page)
            .into_iter()
            .map(String::from)
            .collect();
        let coords: Vec<(String, String)> = page_cats
            .iter()
            .map(|cat| {
                let sel = view.page_selection(cat).unwrap_or("").to_string();
                (cat.clone(), sel)
            })
            .filter(|(_, v)| !v.is_empty())
            .collect();
        let key = crate::model::cell::CellKey::new(coords);
        vec![
            Box::new(effect::SetCell(key, CellValue::Number(0.0))),
            effect::mark_dirty(),
            effect::set_status("Added new record row"),
        ]
    }
}
