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

use super::core::{Cmd, CmdContext};
use super::grid::AddRecordRow;
use super::navigation::{CursorState, EnterAdvance, viewport_effects};

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::*;
    use crate::command::cmd::test_helpers::*;
    use crate::workbook::Workbook;

    #[test]
    fn commit_formula_with_categories_adds_formula() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("formula".to_string(), "Profit = Revenue - Cost".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = CommitFormula.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("AddFormula"),
            "Expected AddFormula, got: {dbg}"
        );
        assert!(
            dbg.contains("FormulaPanel"),
            "Expected return to FormulaPanel, got: {dbg}"
        );
    }

    /// Formulas always target _Measure by default, even when no regular
    /// categories exist. _Measure is a virtual category that always exists.
    #[test]
    fn commit_formula_without_regular_categories_targets_measure() {
        let m = Workbook::new("Empty");
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("formula".to_string(), "X = Y + Z".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = CommitFormula.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("AddFormula"),
            "Should add formula targeting _Measure, got: {dbg}"
        );
        assert!(
            dbg.contains("_Measure"),
            "target_category should be _Measure, got: {dbg}"
        );
    }

    #[test]
    fn commit_category_add_with_name_produces_add_effect() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("category".to_string(), "Region".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = CommitCategoryAdd.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("AddCategory"),
            "Expected AddCategory, got: {dbg}"
        );
    }

    #[test]
    fn commit_category_add_with_empty_buffer_returns_to_panel() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("category".to_string(), "".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = CommitCategoryAdd.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("CategoryPanel"),
            "Expected return to CategoryPanel, got: {dbg}"
        );
    }

    #[test]
    fn commit_item_add_with_name_produces_add_item() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("item".to_string(), "March".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        let item_add_mode = AppMode::item_add("Month".to_string());
        ctx.mode = &item_add_mode;
        ctx.buffers = &bufs;
        let effects = CommitItemAdd.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("AddItem"), "Expected AddItem, got: {dbg}");
    }

    #[test]
    fn commit_item_add_outside_item_add_mode_returns_empty() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = CommitItemAdd.execute(&ctx);
        assert!(effects.is_empty());
    }

    /// `CommitAndAdvance` must thread its `edit_mode` through to the
    /// trailing `EnterEditAtCursor` effect so the post-commit re-edit lands
    /// in the mode the keymap requested. The command never reads ctx.mode.
    #[test]
    fn commit_and_advance_threads_edit_mode_to_enter_edit_at_cursor() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("edit".to_string(), "42".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        // ctx.mode stays Normal — the command must not look at it.
        let key = ctx.cell_key().unwrap();
        let cmd = CommitAndAdvance {
            key,
            value: "42".to_string(),
            advance: super::AdvanceDir::Down,
            cursor: super::CursorState::from_ctx(&ctx),
            edit_mode: AppMode::records_editing(),
        };
        let effects = cmd.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("EnterEditAtCursor") && dbg.contains("RecordsEditing"),
            "Expected trailing EnterEditAtCursor with RecordsEditing target, got: {dbg}"
        );
    }

    #[test]
    fn commit_export_produces_export_and_normal_mode() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("export".to_string(), "/tmp/test.csv".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = CommitExport.execute(&ctx);
        assert_eq!(effects.len(), 2);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("ExportCsv"), "Expected ExportCsv, got: {dbg}");
        assert!(dbg.contains("Normal"), "Expected Normal mode, got: {dbg}");
    }
}

// ── Commit commands (mode-specific buffer consumers) ────────────────────────

/// Commit a cell value: for synthetic records keys, stage in drill pending edits
/// in drill mode, or apply directly in plain records mode; for real keys, write
/// to the model.
fn commit_regular_cell_value(key: &CellKey, value: &str, effects: &mut Vec<Box<dyn Effect>>) {
    if value.is_empty() {
        effects.push(Box::new(effect::ClearCell(key.clone())));
    } else if let Ok(n) = value.parse::<f64>() {
        effects.push(Box::new(effect::SetCell(key.clone(), CellValue::Number(n))));
    } else {
        effects.push(Box::new(effect::SetCell(
            key.clone(),
            CellValue::Text(value.to_string()),
        )));
    }
    effects.push(effect::mark_dirty());
}

/// Stage a synthetic edit in drill state so it can be applied atomically on exit.
fn stage_drill_edit(record_idx: usize, col_name: String, value: &str) -> Box<dyn Effect> {
    Box::new(effect::SetDrillPendingEdit {
        record_idx,
        col_name,
        new_value: value.to_string(),
    })
}

/// Apply a synthetic records-mode edit directly to the underlying model cell.
fn commit_plain_records_edit(
    ctx: &CmdContext,
    record_idx: usize,
    col_name: &str,
    value: &str,
    effects: &mut Vec<Box<dyn Effect>>,
) {
    let Some((orig_key, _)) = ctx
        .layout
        .records
        .as_ref()
        .and_then(|records| records.get(record_idx))
    else {
        return;
    };

    if col_name == "Value" {
        commit_regular_cell_value(orig_key, value, effects);
        return;
    }

    if value.is_empty() {
        effects.push(effect::set_status(effect::RECORD_COORDS_CANNOT_BE_EMPTY));
        return;
    }

    let Some(existing_value) = ctx.model.get_cell(orig_key).cloned() else {
        return;
    };
    effects.push(Box::new(effect::ClearCell(orig_key.clone())));
    effects.push(Box::new(effect::AddItem {
        category: col_name.to_string(),
        item: value.to_string(),
    }));
    effects.push(Box::new(effect::SetCell(
        orig_key.clone().with(col_name, value),
        existing_value,
    )));
    effects.push(effect::mark_dirty());
}

fn commit_cell_value(
    ctx: &CmdContext,
    key: &CellKey,
    value: &str,
    effects: &mut Vec<Box<dyn Effect>>,
) {
    if let Some((record_idx, col_name)) = crate::view::synthetic_record_info(key) {
        if ctx.has_drill_state {
            effects.push(stage_drill_edit(record_idx, col_name, value));
            return;
        }
        commit_plain_records_edit(ctx, record_idx, &col_name, value, effects);
        return;
    }

    commit_regular_cell_value(key, value, effects);
}

/// Direction to advance after committing a cell edit.
#[derive(Debug, Clone, Copy)]
pub enum AdvanceDir {
    /// Move down (typewriter-style, wraps to next column at bottom).
    Down,
    /// Move right (clamps at rightmost column).
    Right,
}

/// Return the normal-mode counterpart of an editing mode. Used by
/// `CommitAndAdvance` to compute the mode to land in if the advance
/// aborts (commit + exit editing at boundary).
fn exit_mode_for(edit_mode: &AppMode) -> AppMode {
    match edit_mode {
        AppMode::RecordsEditing { .. } => AppMode::RecordsNormal,
        _ => AppMode::Normal,
    }
}

/// Commit a cell edit, advance the cursor, and re-enter edit mode.
/// Subsumes the old `CommitCellEdit` (Down) and `CommitAndAdvanceRight` (Right).
///
/// `edit_mode` is the editing mode to re-enter after advancing. The keymap
/// binding supplies this — the editing-mode keymap passes `editing` and the
/// records-editing keymap passes `records-editing`. The command itself
/// never inspects `ctx.mode`.
#[derive(Debug)]
pub struct CommitAndAdvance {
    pub key: CellKey,
    pub value: String,
    pub advance: AdvanceDir,
    pub cursor: CursorState,
    pub edit_mode: AppMode,
}
impl Cmd for CommitAndAdvance {
    fn name(&self) -> &'static str {
        match self.advance {
            AdvanceDir::Down => "commit-cell-edit",
            AdvanceDir::Right => "commit-and-advance-right",
        }
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let mut effects: Vec<Box<dyn Effect>> = Vec::new();
        commit_cell_value(ctx, &self.key, &self.value, &mut effects);
        // Pre-emptively drop to the normal counterpart of edit_mode. If the
        // advance succeeds, the trailing `EnterEditAtCursor` below will lift
        // us back into editing on the new cell. If the advance aborts
        // (e.g. already at bottom-right on Enter), `EnterEditAtCursor` is
        // skipped and we land in normal mode — which is the desired
        // "Enter at bottom-right commits and exits" behavior.
        effects.push(effect::change_mode(exit_mode_for(&self.edit_mode)));
        match self.advance {
            AdvanceDir::Down => {
                let adv = EnterAdvance {
                    cursor: self.cursor.clone(),
                };
                effects.extend(adv.execute(ctx));
            }
            AdvanceDir::Right => {
                let col_max = self.cursor.col_count.saturating_sub(1);
                let row_max = self.cursor.row_count.saturating_sub(1);
                let at_bottom_right = self.cursor.row >= row_max && self.cursor.col >= col_max;

                if at_bottom_right && ctx.is_records_mode() {
                    let add = AddRecordRow;
                    effects.extend(add.execute(ctx));
                    effects.extend(viewport_effects(
                        self.cursor.row + 1,
                        0,
                        self.cursor.row_offset,
                        self.cursor.col_offset,
                        self.cursor.visible_rows,
                        self.cursor.visible_cols,
                    ));
                } else {
                    let nc = (self.cursor.col + 1).min(col_max);
                    effects.extend(viewport_effects(
                        self.cursor.row,
                        nc,
                        self.cursor.row_offset,
                        self.cursor.col_offset,
                        self.cursor.visible_rows,
                        self.cursor.visible_cols,
                    ));
                }
            }
        }
        effects.push(Box::new(effect::EnterEditAtCursor {
            target_mode: self.edit_mode.clone(),
        }));
        effects
    }
}

/// Commit a formula from the formula edit buffer.
#[derive(Debug)]
pub struct CommitFormula;
impl Cmd for CommitFormula {
    fn name(&self) -> &'static str {
        "commit-formula"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let buf = ctx.buffers.get("formula").cloned().unwrap_or_default();
        // Default formula target to _Measure (the virtual measure category).
        // _Measure dynamically includes all formula targets.
        vec![
            Box::new(effect::AddFormula {
                raw: buf,
                target_category: "_Measure".to_string(),
            }),
            effect::mark_dirty(),
            effect::set_status("Formula added"),
            effect::change_mode(AppMode::FormulaPanel),
        ]
    }
}

/// Shared helper: read a buffer, trim it, and if non-empty, produce add + dirty
/// + status effects. If empty, return to CategoryPanel.
///
/// Buffer clearing is handled by the keymap (Enter → [commit, clear-buffer]).
fn commit_add_from_buffer(
    ctx: &CmdContext,
    buffer_name: &str,
    add_effect: impl FnOnce(&str) -> Option<Box<dyn Effect>>,
    status_msg: impl FnOnce(&str) -> String,
) -> Vec<Box<dyn Effect>> {
    let buf = ctx.buffers.get(buffer_name).cloned().unwrap_or_default();
    let trimmed = buf.trim().to_string();
    if trimmed.is_empty() {
        return vec![effect::change_mode(AppMode::CategoryPanel)];
    }
    let Some(add) = add_effect(&trimmed) else {
        return vec![];
    };
    vec![
        add,
        effect::mark_dirty(),
        effect::set_status(status_msg(&trimmed)),
    ]
}

/// Commit adding a category, staying in CategoryAdd mode for the next entry.
#[derive(Debug)]
pub struct CommitCategoryAdd;
impl Cmd for CommitCategoryAdd {
    fn name(&self) -> &'static str {
        "commit-category-add"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        commit_add_from_buffer(
            ctx,
            "category",
            |name| Some(Box::new(effect::AddCategory(name.to_string()))),
            |name| format!("Added category \"{name}\""),
        )
    }
}

/// Commit adding an item, staying in ItemAdd mode for the next entry.
#[derive(Debug)]
pub struct CommitItemAdd;
impl Cmd for CommitItemAdd {
    fn name(&self) -> &'static str {
        "commit-item-add"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let category = if let AppMode::ItemAdd { category, .. } = ctx.mode {
            category.clone()
        } else {
            return vec![];
        };
        commit_add_from_buffer(
            ctx,
            "item",
            |name| {
                Some(Box::new(effect::AddItem {
                    category: category.clone(),
                    item: name.to_string(),
                }))
            },
            |name| format!("Added \"{name}\""),
        )
    }
}

/// Commit an export from the export buffer.
#[derive(Debug)]
pub struct CommitExport;
impl Cmd for CommitExport {
    fn name(&self) -> &'static str {
        "commit-export"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let buf = ctx.buffers.get("export").cloned().unwrap_or_default();
        vec![
            Box::new(effect::ExportCsv(std::path::PathBuf::from(buf))),
            effect::change_mode(AppMode::Normal),
        ]
    }
}
