use std::collections::HashMap;
use std::fmt::Debug;

use crossterm::event::KeyCode;

use crate::model::Model;
use crate::model::cell::{CellKey, CellValue};
use crate::ui::app::AppMode;
use crate::ui::effect::{Effect, Panel};
use crate::view::{Axis, GridLayout, View};
use crate::workbook::Workbook;

/// Read-only context available to commands for decision-making.
///
/// Commands receive a `&Model` (pure data) and a `&View` (the active view).
/// The full `&Workbook` is also available for the rare command that needs
/// to enumerate all views (e.g. the view panel).
pub struct CmdContext<'a> {
    pub model: &'a Model,
    pub workbook: &'a Workbook,
    pub view: &'a View,
    pub layout: &'a GridLayout,
    pub registry: &'a CmdRegistry,
    pub mode: &'a AppMode,
    pub selected: (usize, usize),
    pub row_offset: usize,
    pub col_offset: usize,
    pub search_query: &'a str,
    pub yanked: &'a Option<CellValue>,
    pub dirty: bool,
    pub search_mode: bool,
    pub formula_panel_open: bool,
    pub category_panel_open: bool,
    pub view_panel_open: bool,
    /// Panel cursors
    pub formula_cursor: usize,
    pub cat_panel_cursor: usize,
    pub view_panel_cursor: usize,
    /// Tile select cursor (which category is selected)
    pub tile_cat_idx: usize,
    /// Named text buffers
    pub buffers: &'a HashMap<String, String>,
    /// View navigation stacks (for drill back/forward)
    pub view_back_stack: &'a [crate::ui::app::ViewFrame],
    pub view_forward_stack: &'a [crate::ui::app::ViewFrame],
    /// Whether the app currently has an active drill snapshot.
    pub has_drill_state: bool,
    /// Display value at the cursor — works uniformly for pivot and records mode.
    pub display_value: String,
    /// How many data rows/cols fit on screen (for viewport scrolling).
    pub visible_rows: usize,
    pub visible_cols: usize,
    /// Expanded categories in the tree panel
    pub expanded_cats: &'a std::collections::HashSet<String>,
    /// The key that triggered this command
    pub key_code: KeyCode,
}

impl<'a> CmdContext<'a> {
    /// Return true when the current layout is a records-mode layout.
    pub fn is_records_mode(&self) -> bool {
        self.layout.is_records_mode()
    }

    pub fn cell_key(&self) -> Option<CellKey> {
        self.layout.cell_key(self.selected.0, self.selected.1)
    }

    /// Return synthetic record coordinates for the current cursor, if any.
    pub fn synthetic_record_at_cursor(&self) -> Option<(usize, String)> {
        self.cell_key()
            .as_ref()
            .and_then(crate::view::synthetic_record_info)
    }

    pub fn row_count(&self) -> usize {
        self.layout.row_count()
    }
    pub fn col_count(&self) -> usize {
        self.layout.col_count()
    }
    pub fn none_cats(&self) -> &[String] {
        &self.layout.none_cats
    }
}

impl<'a> CmdContext<'a> {
    /// Resolve the category panel tree entry at the current cursor.
    pub fn cat_tree_entry(&self) -> Option<crate::ui::cat_tree::CatTreeEntry> {
        let tree = crate::ui::cat_tree::build_cat_tree(self.model, self.expanded_cats);
        tree.into_iter().nth(self.cat_panel_cursor)
    }

    /// The category name at the current tree cursor (whether on a
    /// category header or an item).
    pub fn cat_at_cursor(&self) -> Option<String> {
        self.cat_tree_entry().map(|e| e.cat_name().to_string())
    }

    /// Total number of entries in the category tree.
    pub fn cat_tree_len(&self) -> usize {
        crate::ui::cat_tree::build_cat_tree(self.model, self.expanded_cats).len()
    }
}

/// A command that reads state and produces effects.
pub trait Cmd: Debug + Send + Sync {
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>>;
    /// The canonical name of this command (matches its registry key).
    /// Used by the parser tests and for introspection.
    #[allow(dead_code)]
    fn name(&self) -> &'static str;
}

/// Factory that constructs a Cmd from text arguments (headless/script).
pub type ParseFn = fn(&[String]) -> Result<Box<dyn Cmd>, String>;

/// Factory that constructs a Cmd from the interactive context (keymap dispatch).
/// Receives both the keymap args and the interactive context so commands can
/// combine text arguments (e.g. panel name) with runtime state (e.g. whether
/// the panel is currently open).
pub type InteractiveFn = fn(&[String], &CmdContext) -> Result<Box<dyn Cmd>, String>;

type BoxParseFn = Box<dyn Fn(&[String]) -> Result<Box<dyn Cmd>, String>>;
type BoxInteractiveFn = Box<dyn Fn(&[String], &CmdContext) -> Result<Box<dyn Cmd>, String>>;

/// A registered command entry with both text and interactive constructors.
struct CmdEntry {
    name: &'static str,
    parse: BoxParseFn,
    interactive: BoxInteractiveFn,
}

/// Registry of commands constructible from text or from interactive context.
#[derive(Default)]
pub struct CmdRegistry {
    entries: Vec<CmdEntry>,
    aliases: Vec<(&'static str, &'static str)>,
}

impl CmdRegistry {
    pub fn new() -> Self {
        Self {
            entries: Vec::new(),
            aliases: Vec::new(),
        }
    }

    /// Register a short name that resolves to a canonical command name.
    pub fn alias(&mut self, short: &'static str, canonical: &'static str) {
        self.aliases.push((short, canonical));
    }

    /// Resolve a command name through the alias table.
    fn resolve<'a>(&'a self, name: &'a str) -> &'a str {
        for (alias, canonical) in &self.aliases {
            if *alias == name {
                return canonical;
            }
        }
        name
    }

    /// Register a command with both a text parser and an interactive constructor.
    /// The name is derived from a prototype command instance.
    pub fn register(&mut self, prototype: &dyn Cmd, parse: ParseFn, interactive: InteractiveFn) {
        self.entries.push(CmdEntry {
            name: prototype.name(),
            parse: Box::new(parse),
            interactive: Box::new(interactive),
        });
    }

    /// Register a command that doesn't need interactive context.
    /// When called interactively with args, delegates to parse.
    /// When called interactively without args, returns an error.
    pub fn register_pure(&mut self, prototype: &dyn Cmd, parse: ParseFn) {
        self.entries.push(CmdEntry {
            name: prototype.name(),
            parse: Box::new(parse),
            interactive: Box::new(move |args, _ctx| {
                if args.is_empty() {
                    Err("this command requires arguments".into())
                } else {
                    parse(args)
                }
            }),
        });
    }

    /// Register a zero-arg command (same instance for parse and interactive).
    /// The name is derived by calling `f()` once.
    pub fn register_nullary(&mut self, f: fn() -> Box<dyn Cmd>) {
        let name = f().name();
        self.entries.push(CmdEntry {
            name,
            parse: Box::new(move |_| Ok(f())),
            interactive: Box::new(move |_, _| Ok(f())),
        });
    }

    /// Construct a command from text arguments (script/headless).
    pub fn parse(&self, name: &str, args: &[String]) -> Result<Box<dyn Cmd>, String> {
        let name = self.resolve(name);
        for e in &self.entries {
            if e.name == name {
                return (e.parse)(args);
            }
        }
        Err(format!("Unknown command: {name}"))
    }

    /// Construct a command from interactive context (keymap dispatch).
    /// Always calls the interactive constructor with both args and ctx,
    /// so commands can combine text arguments with runtime state.
    pub fn interactive(
        &self,
        name: &str,
        args: &[String],
        ctx: &CmdContext,
    ) -> Result<Box<dyn Cmd>, String> {
        let name = self.resolve(name);
        for e in &self.entries {
            if e.name == name {
                return (e.interactive)(args, ctx);
            }
        }
        Err(format!("Unknown command: {name}"))
    }

    #[allow(dead_code)]
    pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
        self.entries.iter().map(|e| e.name)
    }
}

/// Dummy prototype used only for name extraction in registry calls
/// where the real command struct is built by a closure.
#[derive(Debug)]
pub(super) struct NamedCmd(pub(super) &'static str);
impl Cmd for NamedCmd {
    fn name(&self) -> &'static str {
        self.0
    }
    fn execute(&self, _: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![]
    }
}

pub(super) fn require_args(word: &str, args: &[String], n: usize) -> Result<(), String> {
    if args.len() < n {
        Err(format!(
            "{word} requires {n} argument(s), got {}",
            args.len()
        ))
    } else {
        Ok(())
    }
}

/// Parse Cat/Item coordinate args into a CellKey.
pub(super) fn parse_cell_key_from_args(args: &[String]) -> crate::model::cell::CellKey {
    let coords: Vec<(String, String)> = args
        .iter()
        .filter_map(|s| {
            let (cat, item) = s.split_once('/')?;
            Some((cat.to_string(), item.to_string()))
        })
        .collect();
    crate::model::cell::CellKey::new(coords)
}

/// Read the current value of a named buffer from context.
pub(super) fn read_buffer(ctx: &CmdContext, name: &str) -> String {
    if name == "search" {
        ctx.search_query.to_string()
    } else {
        ctx.buffers.get(name).cloned().unwrap_or_default()
    }
}

pub(super) fn parse_panel(s: &str) -> Result<Panel, String> {
    match s {
        "formula" => Ok(Panel::Formula),
        "category" => Ok(Panel::Category),
        "view" => Ok(Panel::View),
        other => Err(format!("Unknown panel: {other}")),
    }
}

pub(super) fn parse_axis(s: &str) -> Result<Axis, String> {
    match s.to_lowercase().as_str() {
        "row" => Ok(Axis::Row),
        "column" | "col" => Ok(Axis::Column),
        "page" => Ok(Axis::Page),
        "none" => Ok(Axis::None),
        other => Err(format!("Unknown axis: {other}")),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_axis_recognizes_all_variants() {
        assert!(parse_axis("row").is_ok());
        assert!(parse_axis("column").is_ok());
        assert!(parse_axis("col").is_ok());
        assert!(parse_axis("page").is_ok());
        assert!(parse_axis("none").is_ok());
        assert!(parse_axis("ROW").is_ok());
    }

    #[test]
    fn parse_axis_rejects_unknown() {
        assert!(parse_axis("diagonal").is_err());
    }
}
