use crate::ui::effect::{self, Effect};
use crate::view::Axis;

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

// ── Navigation commands ──────────────────────────────────────────────────────
// All navigation commands take explicit cursor state. The interactive spec
// fills position/bounds from context; the parser accepts them as args.

/// Shared viewport state for navigation commands.
#[derive(Debug, Clone, Default)]
pub struct CursorState {
    pub row: usize,
    pub col: usize,
    pub row_count: usize,
    pub col_count: usize,
    pub row_offset: usize,
    pub col_offset: usize,
    pub visible_rows: usize,
    pub visible_cols: usize,
}

impl CursorState {
    pub fn from_ctx(ctx: &CmdContext) -> Self {
        Self {
            row: ctx.selected.0,
            col: ctx.selected.1,
            row_count: ctx.row_count(),
            col_count: ctx.col_count(),
            row_offset: ctx.row_offset,
            col_offset: ctx.col_offset,
            visible_rows: ctx.visible_rows,
            visible_cols: ctx.visible_cols,
        }
    }
}

/// Compute viewport-tracking effects for a new row/col position.
pub(super) fn viewport_effects(
    nr: usize,
    nc: usize,
    old_row_offset: usize,
    old_col_offset: usize,
    visible_rows: usize,
    visible_cols: usize,
) -> Vec<Box<dyn Effect>> {
    let mut effects: Vec<Box<dyn Effect>> = vec![effect::set_selected(nr, nc)];
    let mut row_offset = old_row_offset;
    let mut col_offset = old_col_offset;
    let vr = visible_rows.max(1);
    let vc = visible_cols.max(1);
    if nr < row_offset {
        row_offset = nr;
    }
    if nr >= row_offset + vr {
        row_offset = nr.saturating_sub(vr - 1);
    }
    if nc < col_offset {
        col_offset = nc;
    }
    if nc >= col_offset + vc {
        col_offset = nc.saturating_sub(vc - 1);
    }
    if row_offset != old_row_offset {
        effects.push(Box::new(effect::SetRowOffset(row_offset)));
    }
    if col_offset != old_col_offset {
        effects.push(Box::new(effect::SetColOffset(col_offset)));
    }
    effects
}

/// How to move the cursor.
#[derive(Debug, Clone)]
pub enum MoveKind {
    /// Relative offset (dr, dc) — subsumes MoveSelection and ScrollRows.
    Relative(i32, i32),
    /// Jump to start of axis: `true` = row, `false` = col.
    ToStart(bool),
    /// Jump to end of axis: `true` = row, `false` = col.
    ToEnd(bool),
    /// Page scroll: +1 = down, -1 = up (delta computed from visible_rows).
    Page(i32),
}

/// Unified navigation command. All variants go through `viewport_effects`.
#[derive(Debug)]
pub struct Move {
    pub kind: MoveKind,
    pub cursor: CursorState,
    pub cmd_name: &'static str,
}

impl Cmd for Move {
    fn name(&self) -> &'static str {
        self.cmd_name
    }

    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let row_max = self.cursor.row_count.saturating_sub(1) as i32;
        let col_max = self.cursor.col_count.saturating_sub(1) as i32;
        let (nr, nc) = match &self.kind {
            MoveKind::Relative(dr, dc) => {
                let nr = (self.cursor.row as i32 + dr).clamp(0, row_max) as usize;
                let nc = (self.cursor.col as i32 + dc).clamp(0, col_max) as usize;
                (nr, nc)
            }
            MoveKind::ToStart(is_row) => {
                if *is_row {
                    (0, self.cursor.col)
                } else {
                    (self.cursor.row, 0)
                }
            }
            MoveKind::ToEnd(is_row) => {
                if *is_row {
                    (row_max.max(0) as usize, self.cursor.col)
                } else {
                    (self.cursor.row, col_max.max(0) as usize)
                }
            }
            MoveKind::Page(dir) => {
                let delta = (self.cursor.visible_rows as i32 * 3 / 4).max(1) * dir;
                let nr = (self.cursor.row as i32 + delta).clamp(0, row_max) as usize;
                (nr, self.cursor.col)
            }
        };
        viewport_effects(
            nr,
            nc,
            self.cursor.row_offset,
            self.cursor.col_offset,
            self.cursor.visible_rows,
            self.cursor.visible_cols,
        )
    }
}

/// Typewriter-style advance: move down, wrap to top of next column at bottom.
#[derive(Debug)]
pub struct EnterAdvance {
    pub cursor: CursorState,
}
impl Cmd for EnterAdvance {
    fn name(&self) -> &'static str {
        "enter-advance"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let row_max = self.cursor.row_count.saturating_sub(1);
        let col_max = self.cursor.col_count.saturating_sub(1);
        let (r, c) = (self.cursor.row, self.cursor.col);
        let (nr, nc) = if r < row_max {
            (r + 1, c)
        } else if c < col_max {
            (0, c + 1)
        } else {
            // Already at bottom-right — the advance premise no longer holds.
            // Abort the rest of the batch so the caller's trailing effects
            // (e.g. `CommitAndAdvance`'s `EnterEditAtCursor`) are skipped.
            return vec![Box::new(effect::AbortChain)];
        };
        viewport_effects(
            nr,
            nc,
            self.cursor.row_offset,
            self.cursor.col_offset,
            self.cursor.visible_rows,
            self.cursor.visible_cols,
        )
    }
}

// ── Page navigation ─────────────────────────────────────────────────────

/// Advance to the next page (odometer-style cycling).
#[derive(Debug)]
pub struct PageNext;
impl Cmd for PageNext {
    fn name(&self) -> &'static str {
        "page-next"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let data = page_cat_data(ctx);
        if data.is_empty() {
            return vec![];
        }
        let mut indices: Vec<usize> = data.iter().map(|(_, _, i)| *i).collect();
        let mut carry = true;
        for i in (0..data.len()).rev() {
            if !carry {
                break;
            }
            indices[i] += 1;
            if indices[i] >= data[i].1.len() {
                indices[i] = 0;
            } else {
                carry = false;
            }
        }
        data.iter()
            .enumerate()
            .map(|(i, (cat, items, _))| {
                Box::new(effect::SetPageSelection {
                    category: cat.clone(),
                    item: items[indices[i]].clone(),
                }) as Box<dyn Effect>
            })
            .collect()
    }
}

/// Go to the previous page (odometer-style cycling).
#[derive(Debug)]
pub struct PagePrev;
impl Cmd for PagePrev {
    fn name(&self) -> &'static str {
        "page-prev"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let data = page_cat_data(ctx);
        if data.is_empty() {
            return vec![];
        }
        let mut indices: Vec<usize> = data.iter().map(|(_, _, i)| *i).collect();
        let mut borrow = true;
        for i in (0..data.len()).rev() {
            if !borrow {
                break;
            }
            if indices[i] == 0 {
                indices[i] = data[i].1.len().saturating_sub(1);
            } else {
                indices[i] -= 1;
                borrow = false;
            }
        }
        data.iter()
            .enumerate()
            .map(|(i, (cat, items, _))| {
                Box::new(effect::SetPageSelection {
                    category: cat.clone(),
                    item: items[indices[i]].clone(),
                }) as Box<dyn Effect>
            })
            .collect()
    }
}

/// Gather (cat_name, items, current_idx) for page-axis categories.
pub(super) fn page_cat_data(ctx: &CmdContext) -> Vec<(String, Vec<String>, usize)> {
    let view = ctx.view;
    let page_cats: Vec<String> = view
        .categories_on(Axis::Page)
        .into_iter()
        .map(String::from)
        .collect();
    page_cats
        .into_iter()
        .filter_map(|cat| {
            let items: Vec<String> = ctx
                .model
                .category(&cat)
                .map(|c| {
                    c.ordered_item_names()
                        .into_iter()
                        .map(String::from)
                        .collect()
                })
                .unwrap_or_default();
            if items.is_empty() {
                return None;
            }
            let current = view
                .page_selection(&cat)
                .map(String::from)
                .or_else(|| items.first().cloned())
                .unwrap_or_default();
            let idx = items.iter().position(|i| *i == current).unwrap_or(0);
            Some((cat, items, idx))
        })
        .collect()
}

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

    #[test]
    fn move_selection_down_produces_set_selected() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = Move {
            kind: MoveKind::Relative(1, 0),
            cursor: CursorState::from_ctx(&ctx),
            cmd_name: "move-selection",
        };
        let effects = cmd.execute(&ctx);
        assert!(!effects.is_empty());
    }

    #[test]
    fn move_selection_clamps_to_bounds() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = Move {
            kind: MoveKind::Relative(100, 100),
            cursor: CursorState::from_ctx(&ctx),
            cmd_name: "move-selection",
        };
        let effects = cmd.execute(&ctx);
        assert!(!effects.is_empty());
    }

    #[test]
    fn enter_advance_moves_down() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = EnterAdvance {
            cursor: CursorState::from_ctx(&ctx),
        };
        let effects = cmd.execute(&ctx);
        assert!(!effects.is_empty());
        let dbg = format!("{:?}", effects[0]);
        assert!(
            dbg.contains("SetSelected(1, 0)"),
            "Expected row 1, got: {dbg}"
        );
    }

    /// At bottom-right `EnterAdvance` has no place to go, so it emits a
    /// single `AbortChain` effect. Trailing effects in a `CommitAndAdvance`
    /// batch (e.g. `EnterEditAtCursor`) are then skipped, which is how
    /// "Enter at bottom-right commits and exits editing" is realised.
    #[test]
    fn enter_advance_at_bottom_right_emits_abort_chain() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let mut cursor = CursorState::from_ctx(&ctx);
        cursor.row = cursor.row_count.saturating_sub(1);
        cursor.col = cursor.col_count.saturating_sub(1);
        let cmd = EnterAdvance { cursor };
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 1, "should emit exactly AbortChain");
        let dbg = format!("{:?}", effects[0]);
        assert!(
            dbg.contains("AbortChain"),
            "Expected AbortChain, got: {dbg}"
        );
    }

    #[test]
    fn law_move_to_start_idempotent() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = Move {
            kind: MoveKind::ToStart(true),
            cursor: CursorState::from_ctx(&ctx),
            cmd_name: "jump-first-row",
        };
        let first = effects_debug(&cmd.execute(&ctx));
        let cmd2 = Move {
            kind: MoveKind::ToStart(true),
            cursor: CursorState {
                row: 0,
                ..CursorState::from_ctx(&ctx)
            },
            cmd_name: "jump-first-row",
        };
        let second = effects_debug(&cmd2.execute(&ctx));
        assert_eq!(first, second, "ToStart(Row) should be idempotent");
    }

    #[test]
    fn law_sequence_associativity() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);

        let mk_a = || {
            Move {
                kind: MoveKind::Relative(1, 0),
                cursor: CursorState::from_ctx(&ctx),
                cmd_name: "move-selection",
            }
            .execute(&ctx)
        };
        let mk_b = || {
            Move {
                kind: MoveKind::Relative(0, 1),
                cursor: CursorState::from_ctx(&ctx),
                cmd_name: "move-selection",
            }
            .execute(&ctx)
        };
        let mk_c = || {
            Move {
                kind: MoveKind::ToStart(true),
                cursor: CursorState::from_ctx(&ctx),
                cmd_name: "jump-first-row",
            }
            .execute(&ctx)
        };

        let mut ab_c = mk_a();
        ab_c.extend(mk_b());
        ab_c.extend(mk_c());

        let mut bc = mk_b();
        bc.extend(mk_c());
        let mut a_bc = mk_a();
        a_bc.extend(bc);

        assert_eq!(
            effects_debug(&ab_c),
            effects_debug(&a_bc),
            "Sequence concatenation should be associative"
        );
    }

    #[test]
    fn law_move_to_end_reaches_last_col() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = Move {
            kind: MoveKind::ToEnd(false),
            cursor: CursorState::from_ctx(&ctx),
            cmd_name: "jump-last-col",
        };
        let effects = cmd.execute(&ctx);
        let dbg = effects_debug(&effects);
        let expected_col = ctx.col_count().saturating_sub(1);
        assert!(
            dbg.contains(&format!("SetSelected(0, {expected_col})")),
            "Expected jump to last col {expected_col}, got: {dbg}"
        );
    }

    #[test]
    fn page_next_with_no_page_cats_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 = PageNext.execute(&ctx);
        assert!(effects.is_empty());
    }

    #[test]
    fn page_prev_with_no_page_cats_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 = PagePrev.execute(&ctx);
        assert!(effects.is_empty());
    }

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

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