//! Shortcut picker construction for `/keymap`.

use codex_config::types::TuiKeymap;
use ratatui::style::Styled;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::text::Span;
use unicode_width::UnicodeWidthStr;

use crate::app_event::AppEvent;
use crate::bottom_pane::ColumnWidthMode;
use crate::bottom_pane::SelectionItem;
use crate::bottom_pane::SelectionRowDisplay;
use crate::bottom_pane::SelectionTab;
use crate::bottom_pane::SelectionViewParams;
use crate::keymap::RuntimeKeymap;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use crate::style::accent_style;

use super::actions::KEYMAP_ACTIONS;
use super::actions::KeymapActionFilter;
use super::actions::action_label;
use super::actions::bindings_for_action;
use super::actions::format_binding_summary;
use super::has_custom_binding;

pub(crate) const KEYMAP_PICKER_VIEW_ID: &str = "keymap-picker";
pub(super) const KEYMAP_ALL_TAB_ID: &str = "all-shortcuts";
pub(super) const KEYMAP_COMMON_TAB_ID: &str = "common-shortcuts";
pub(super) const KEYMAP_CUSTOM_TAB_ID: &str = "custom-shortcuts";
pub(super) const KEYMAP_UNBOUND_TAB_ID: &str = "unbound-shortcuts";
pub(super) const KEYMAP_DEBUG_TAB_ID: &str = "debug-shortcuts";
const KEYMAP_CONTEXT_LABEL_WIDTH: usize = 12;
const KEYMAP_ROW_PREFIX_WIDTH: usize = KEYMAP_CONTEXT_LABEL_WIDTH + 3;

#[derive(Clone, Debug)]
struct KeymapActionRow {
    context: &'static str,
    context_label: &'static str,
    action: &'static str,
    label: String,
    description: &'static str,
    binding_summary: String,
    custom_binding: bool,
}

impl KeymapActionRow {
    fn is_unbound(&self) -> bool {
        self.binding_summary == "unbound"
    }
}

struct KeymapContextTab {
    id: &'static str,
    label: &'static str,
    description: &'static str,
    contexts: &'static [&'static str],
}

const KEYMAP_COMMON_ACTIONS: &[(&str, &str)] = &[
    ("composer", "submit"),
    ("chat", "interrupt_turn"),
    ("editor", "insert_newline"),
    ("composer", "queue"),
    ("global", "toggle_fast_mode"),
    ("global", "open_external_editor"),
    ("global", "copy"),
    ("global", "toggle_vim_mode"),
    ("editor", "delete_backward_word"),
    ("editor", "delete_forward_word"),
    ("editor", "move_word_left"),
    ("editor", "move_word_right"),
    ("global", "open_transcript"),
    ("pager", "close"),
    ("pager", "page_up"),
    ("pager", "page_down"),
    ("approval", "open_fullscreen"),
    ("approval", "approve"),
    ("approval", "approve_for_session"),
    ("approval", "decline"),
    ("approval", "cancel"),
];

const KEYMAP_CONTEXT_TABS: &[KeymapContextTab] = &[
    KeymapContextTab {
        id: "app-shortcuts",
        label: "App",
        description: "Global and chat-level shortcuts.",
        contexts: &["global", "chat"],
    },
    KeymapContextTab {
        id: "composer-shortcuts",
        label: "Composer",
        description: "Composer submission and queue shortcuts.",
        contexts: &["composer"],
    },
    KeymapContextTab {
        id: "editor-shortcuts",
        label: "Editor",
        description: "Inline editor movement and editing shortcuts.",
        contexts: &["editor"],
    },
    KeymapContextTab {
        id: "vim-shortcuts",
        label: "Vim",
        description: "Vim normal-mode and operator shortcuts.",
        contexts: &["vim_normal", "vim_operator", "vim_text_object"],
    },
    KeymapContextTab {
        id: "navigation-shortcuts",
        label: "Navigation",
        description: "Pager and selection-list navigation shortcuts.",
        contexts: &["pager", "list"],
    },
    KeymapContextTab {
        id: "approval-shortcuts",
        label: "Approval",
        description: "Approval prompt shortcuts.",
        contexts: &["approval"],
    },
];

#[cfg(test)]
pub(crate) fn build_keymap_picker_params(
    runtime_keymap: &RuntimeKeymap,
    keymap_config: &TuiKeymap,
) -> SelectionViewParams {
    build_keymap_picker_params_with_filter(
        runtime_keymap,
        keymap_config,
        KeymapActionFilter::default(),
    )
}

pub(crate) fn build_keymap_picker_params_with_filter(
    runtime_keymap: &RuntimeKeymap,
    keymap_config: &TuiKeymap,
    action_filter: KeymapActionFilter,
) -> SelectionViewParams {
    build_keymap_picker_params_for_action(
        runtime_keymap,
        keymap_config,
        action_filter,
        /*selected_action*/ None,
    )
}

#[cfg(test)]
pub(crate) fn build_keymap_picker_params_for_selected_action(
    runtime_keymap: &RuntimeKeymap,
    keymap_config: &TuiKeymap,
    context: &str,
    action: &str,
) -> SelectionViewParams {
    build_keymap_picker_params_for_selected_action_with_filter(
        runtime_keymap,
        keymap_config,
        KeymapActionFilter::default(),
        context,
        action,
    )
}

pub(crate) fn build_keymap_picker_params_for_selected_action_with_filter(
    runtime_keymap: &RuntimeKeymap,
    keymap_config: &TuiKeymap,
    action_filter: KeymapActionFilter,
    context: &str,
    action: &str,
) -> SelectionViewParams {
    build_keymap_picker_params_for_action(
        runtime_keymap,
        keymap_config,
        action_filter,
        Some((context, action)),
    )
}

fn build_keymap_picker_params_for_action(
    runtime_keymap: &RuntimeKeymap,
    keymap_config: &TuiKeymap,
    action_filter: KeymapActionFilter,
    selected_action: Option<(&str, &str)>,
) -> SelectionViewParams {
    let rows = build_keymap_rows(runtime_keymap, keymap_config, action_filter);
    let total = rows.len();
    let custom_count = rows.iter().filter(|row| row.custom_binding).count();
    let unbound_count = rows.iter().filter(|row| row.is_unbound()).count();
    let initial_selected_idx = selected_action.and_then(|(context, action)| {
        rows.iter()
            .position(|row| row.context == context && row.action == action)
    });
    let name_column_width = rows
        .iter()
        .map(|row| KEYMAP_ROW_PREFIX_WIDTH + UnicodeWidthStr::width(row.label.as_str()))
        .max();

    let mut tabs = Vec::new();
    tabs.push(SelectionTab {
        id: KEYMAP_ALL_TAB_ID.to_string(),
        label: "All".to_string(),
        header: keymap_header(
            "All configurable shortcuts.".to_string(),
            format!("{total} actions, {custom_count} customized, {unbound_count} unbound."),
        ),
        items: keymap_selection_items(
            rows.iter(),
            "No shortcuts available",
            "No configurable shortcuts are available.",
        ),
    });

    let common_rows = keymap_common_rows(&rows);
    let common_count = common_rows.len();
    tabs.push(SelectionTab {
        id: KEYMAP_COMMON_TAB_ID.to_string(),
        label: "Common".to_string(),
        header: keymap_header(
            "Frequently customized shortcuts.".to_string(),
            action_count_line(common_count),
        ),
        items: keymap_selection_items(
            common_rows,
            "No common shortcuts",
            "No common shortcut actions are available.",
        ),
    });

    let custom_rows = rows
        .iter()
        .filter(|row| row.custom_binding)
        .collect::<Vec<_>>();
    tabs.push(SelectionTab {
        id: KEYMAP_CUSTOM_TAB_ID.to_string(),
        label: format!("Customized ({custom_count})"),
        header: keymap_header(
            "Root-level shortcut overrides.".to_string(),
            action_count_line(custom_count),
        ),
        items: keymap_selection_items(
            custom_rows,
            "No customized shortcuts",
            "No root-level keymap overrides have been configured.",
        ),
    });

    let unbound_rows = rows
        .iter()
        .filter(|row| row.is_unbound())
        .collect::<Vec<_>>();
    tabs.push(SelectionTab {
        id: KEYMAP_UNBOUND_TAB_ID.to_string(),
        label: format!("Unbound ({unbound_count})"),
        header: keymap_header(
            "Actions without an active shortcut.".to_string(),
            action_count_line(unbound_count),
        ),
        items: keymap_selection_items(
            unbound_rows,
            "No unbound shortcuts",
            "Every configurable action currently has a shortcut.",
        ),
    });

    for tab in KEYMAP_CONTEXT_TABS {
        let tab_rows = rows
            .iter()
            .filter(|row| tab.contexts.contains(&row.context))
            .collect::<Vec<_>>();
        let count = tab_rows.len();
        tabs.push(SelectionTab {
            id: tab.id.to_string(),
            label: tab.label.to_string(),
            header: keymap_header(tab.description.to_string(), action_count_line(count)),
            items: keymap_selection_items(
                tab_rows,
                "No shortcuts in this group",
                "No configurable actions are available in this group.",
            ),
        });
    }
    tabs.push(keymap_debug_tab());

    SelectionViewParams {
        view_id: Some(KEYMAP_PICKER_VIEW_ID),
        header: Box::new(()),
        footer_hint: Some(keymap_picker_hint_line()),
        tab_footer_hints: vec![(KEYMAP_DEBUG_TAB_ID.to_string(), keymap_debug_hint_line())],
        tabs,
        initial_tab_id: Some(KEYMAP_ALL_TAB_ID.to_string()),
        is_searchable: true,
        search_placeholder: Some("Type to search shortcuts".to_string()),
        col_width_mode: ColumnWidthMode::AutoAllRows,
        row_display: SelectionRowDisplay::SingleLine,
        name_column_width,
        initial_selected_idx,
        ..Default::default()
    }
}

fn keymap_debug_tab() -> SelectionTab {
    SelectionTab {
        id: KEYMAP_DEBUG_TAB_ID.to_string(),
        label: "Debug".to_string(),
        header: keymap_header(
            "Inspect keypresses from your terminal.".to_string(),
            "See the key Codex detects and any shortcuts assigned to it.".to_string(),
        ),
        items: vec![SelectionItem {
            name: "Inspect keypresses".to_string(),
            description: Some(
                "Press Enter to start. Then press any key to inspect it; Ctrl+C exits."
                    .to_string(),
            ),
            selected_description: Some(
                "Open a live inspector that shows the detected key, config key, and matching actions."
                    .to_string(),
            ),
            actions: vec![Box::new(|tx| {
                tx.send(AppEvent::OpenKeymapDebug);
            })],
            search_value: Some("debug inspect keypress key terminal detected actions".to_string()),
            ..Default::default()
        }],
    }
}

fn build_keymap_rows(
    runtime_keymap: &RuntimeKeymap,
    keymap_config: &TuiKeymap,
    action_filter: KeymapActionFilter,
) -> Vec<KeymapActionRow> {
    KEYMAP_ACTIONS
        .iter()
        .copied()
        .filter(|descriptor| descriptor.is_visible(action_filter))
        .map(|descriptor| {
            let bindings =
                bindings_for_action(runtime_keymap, descriptor.context, descriptor.action)
                    .unwrap_or(&[]);
            KeymapActionRow {
                context: descriptor.context,
                context_label: descriptor.context_label,
                action: descriptor.action,
                label: action_label(descriptor.action),
                description: descriptor.description,
                binding_summary: format_binding_summary(bindings),
                custom_binding: has_custom_binding(
                    keymap_config,
                    descriptor.context,
                    descriptor.action,
                )
                .unwrap_or(false),
            }
        })
        .collect()
}

fn keymap_common_rows(rows: &[KeymapActionRow]) -> Vec<&KeymapActionRow> {
    KEYMAP_COMMON_ACTIONS
        .iter()
        .filter_map(|(context, action)| {
            rows.iter()
                .find(|row| row.context == *context && row.action == *action)
        })
        .collect()
}

fn keymap_selection_items<'a>(
    rows: impl IntoIterator<Item = &'a KeymapActionRow>,
    empty_name: &str,
    empty_description: &str,
) -> Vec<SelectionItem> {
    let items = rows
        .into_iter()
        .map(keymap_selection_item)
        .collect::<Vec<_>>();
    if items.is_empty() {
        return vec![SelectionItem {
            name: empty_name.to_string(),
            description: Some(empty_description.to_string()),
            is_disabled: true,
            ..Default::default()
        }];
    }

    items
}

fn keymap_selection_item(row: &KeymapActionRow) -> SelectionItem {
    let context = row.context.to_string();
    let action = row.action.to_string();
    let source = if row.custom_binding {
        "Custom"
    } else {
        "Default"
    };
    let search_value = format!(
        "{} {} {} {} {} {}",
        row.context_label, row.action, row.label, row.description, row.binding_summary, source
    );

    SelectionItem {
        name: row.label.clone(),
        name_prefix_spans: keymap_row_prefix(row),
        description: Some(row.binding_summary.clone()),
        actions: vec![Box::new(move |tx| {
            tx.send(AppEvent::OpenKeymapActionMenu {
                context: context.clone(),
                action: action.clone(),
            });
        })],
        search_value: Some(search_value),
        ..Default::default()
    }
}

fn keymap_row_prefix(row: &KeymapActionRow) -> Vec<Span<'static>> {
    let indicator = if row.custom_binding {
        "*".set_style(accent_style())
    } else if row.is_unbound() {
        "-".dim()
    } else {
        " ".into()
    };

    vec![
        format!(
            "{:<width$} ",
            row.context_label,
            width = KEYMAP_CONTEXT_LABEL_WIDTH
        )
        .dim(),
        indicator,
        " ".dim(),
    ]
}

fn keymap_header(description: String, summary: String) -> Box<dyn Renderable> {
    let mut header = ColumnRenderable::new();
    header.push(Line::from("Keymap".bold()));
    header.push(Line::from(description.dim()));
    header.push(Line::from(summary.dim()));
    Box::new(header)
}

fn action_count_line(count: usize) -> String {
    match count {
        1 => "1 action.".to_string(),
        _ => format!("{count} actions."),
    }
}

fn keymap_picker_hint_line() -> Line<'static> {
    let style = accent_style();
    Line::from(vec![
        "left/right".set_style(style),
        " group · ".dim(),
        "enter".set_style(style),
        " edit shortcut · ".dim(),
        "*".set_style(style),
        " custom · ".dim(),
        "-".set_style(style),
        " unbound · ".dim(),
        "esc".set_style(style),
        " close".dim(),
    ])
}

fn keymap_debug_hint_line() -> Line<'static> {
    let style = accent_style();
    Line::from(vec![
        "enter".set_style(style),
        " start inspector · ".dim(),
        "esc".set_style(style),
        " close".dim(),
    ])
}
