use super::*;

pub(crate) const HERMES_MEMORY_ROW_COUNT: usize = 2;

impl App {
    fn open_openclaw_editor<T: serde::Serialize>(
        &mut self,
        title: &'static str,
        section: Option<&T>,
        submit: EditorSubmit,
    ) {
        let initial = section
            .map(|section| {
                serde_json::to_string_pretty(section).unwrap_or_else(|_| "{}".to_string())
            })
            .unwrap_or_else(|| "{}".to_string());
        self.open_editor(title, EditorKind::Json, initial, submit);
    }

    pub(crate) fn open_daily_memory_filename_prompt(&mut self, initial: String) {
        self.overlay = Overlay::TextInput(TextInputState {
            title: texts::tui_openclaw_daily_memory_create_title().to_string(),
            prompt: texts::tui_openclaw_daily_memory_create_prompt().to_string(),
            input: TextInput::new(initial),
            submit: TextSubmit::OpenClawDailyMemoryFilename,
            secret: false,
        });
    }

    fn today_daily_memory_filename() -> String {
        chrono::Local::now().format("%Y-%m-%d.md").to_string()
    }

    pub(crate) fn on_config_key(&mut self, key: KeyEvent, data: &UiData) -> Action {
        let items = visible_config_items(&self.filter, &self.app_type);
        match key.code {
            KeyCode::Up => {
                self.config_idx = self.config_idx.saturating_sub(1);
                Action::None
            }
            KeyCode::Down => {
                if !items.is_empty() {
                    self.config_idx = (self.config_idx + 1).min(items.len() - 1);
                }
                Action::None
            }
            KeyCode::Char('e') => {
                let Some(item) = items.get(self.config_idx) else {
                    return Action::None;
                };
                if matches!(item, ConfigItem::CommonSnippet) {
                    self.open_common_snippet_editor(
                        self.app_type.clone(),
                        data,
                        None,
                        CommonSnippetViewSource::Global,
                    );
                }
                Action::None
            }
            KeyCode::Enter => {
                let Some(item) = items.get(self.config_idx) else {
                    return Action::None;
                };
                match item {
                    ConfigItem::Path => {
                        self.overlay = Overlay::TextView(TextViewState {
                            title: texts::tui_config_paths_title().to_string(),
                            lines: vec![
                                texts::tui_config_paths_config_file(
                                    &data.config.config_path.display().to_string(),
                                ),
                                texts::tui_config_paths_config_dir(
                                    &data.config.config_dir.display().to_string(),
                                ),
                            ],
                            scroll: 0,
                            action: None,
                        });
                        Action::None
                    }
                    ConfigItem::ShowFull => Action::ConfigShowFull,
                    ConfigItem::Export => {
                        self.overlay = Overlay::TextInput(TextInputState {
                            title: texts::tui_config_export_title().to_string(),
                            prompt: texts::tui_config_export_prompt().to_string(),
                            input: TextInput::new(texts::tui_default_config_export_path()),
                            submit: TextSubmit::ConfigExport,
                            secret: false,
                        });
                        Action::None
                    }
                    ConfigItem::Import => {
                        self.overlay = Overlay::TextInput(TextInputState {
                            title: texts::tui_config_import_title().to_string(),
                            prompt: texts::tui_config_import_prompt().to_string(),
                            input: TextInput::new(texts::tui_default_config_export_path()),
                            submit: TextSubmit::ConfigImport,
                            secret: false,
                        });
                        Action::None
                    }
                    ConfigItem::Backup => {
                        self.overlay = Overlay::TextInput(TextInputState {
                            title: texts::tui_config_backup_title().to_string(),
                            prompt: texts::tui_config_backup_prompt().to_string(),
                            input: TextInput::new(""),
                            submit: TextSubmit::ConfigBackupName,
                            secret: false,
                        });
                        Action::None
                    }
                    ConfigItem::Restore => {
                        if data.config.backups.is_empty() {
                            self.push_toast(texts::tui_toast_no_backups_found(), ToastKind::Info);
                            return Action::None;
                        }
                        self.overlay = Overlay::BackupPicker { selected: 0 };
                        Action::None
                    }
                    ConfigItem::Validate => Action::ConfigValidate,
                    ConfigItem::CommonSnippet => {
                        self.open_common_snippet_editor(
                            self.app_type.clone(),
                            data,
                            None,
                            CommonSnippetViewSource::Global,
                        );
                        Action::None
                    }
                    ConfigItem::Proxy => Action::ConfigOpenProxyHelp,
                    ConfigItem::OpenClawWorkspace
                    | ConfigItem::OpenClawEnv
                    | ConfigItem::OpenClawTools
                    | ConfigItem::OpenClawAgents => self.push_route_and_switch(
                        item.detail_route()
                            .expect("OpenClaw config item should define a detail route"),
                    ),
                    ConfigItem::WebDavSync => self.push_route_and_switch(Route::ConfigWebDav),
                    ConfigItem::Reset => {
                        self.overlay = Overlay::Confirm(ConfirmOverlay {
                            title: texts::tui_config_reset_title().to_string(),
                            message: texts::tui_config_reset_message().to_string(),
                            action: ConfirmAction::ConfigReset,
                        });
                        Action::None
                    }
                }
            }
            _ => Action::None,
        }
    }

    pub(crate) fn on_config_openclaw_workspace_key(
        &mut self,
        key: KeyEvent,
        _data: &UiData,
    ) -> Action {
        match key.code {
            KeyCode::Up => {
                self.workspace_idx = self.workspace_idx.saturating_sub(1);
                Action::None
            }
            KeyCode::Down => {
                self.workspace_idx =
                    (self.workspace_idx + 1).min(openclaw_workspace_entry_count() - 1);
                Action::None
            }
            KeyCode::Enter | KeyCode::Char('e') => {
                match openclaw_workspace_row(self.workspace_idx) {
                    Some(OpenClawWorkspaceRow::DailyMemory) => {
                        self.push_route_and_switch(Route::ConfigOpenClawDailyMemory)
                    }
                    Some(OpenClawWorkspaceRow::File(filename)) => {
                        Action::OpenClawWorkspaceOpenFile {
                            filename: filename.to_string(),
                        }
                    }
                    None => Action::None,
                }
            }
            KeyCode::Char('o') => Action::OpenClawOpenDirectory {
                subdir: String::new(),
            },
            _ => Action::None,
        }
    }

    pub(crate) fn on_config_openclaw_daily_memory_key(
        &mut self,
        key: KeyEvent,
        data: &UiData,
    ) -> Action {
        let visible = visible_openclaw_daily_memory(self, data);
        match key.code {
            KeyCode::Up => {
                self.daily_memory_idx = self.daily_memory_idx.saturating_sub(1);
                Action::None
            }
            KeyCode::Down => {
                if !visible.is_empty() {
                    self.daily_memory_idx = (self.daily_memory_idx + 1).min(visible.len() - 1);
                }
                Action::None
            }
            KeyCode::Enter | KeyCode::Char('e') => {
                let Some(row) = visible.get(self.daily_memory_idx) else {
                    return Action::None;
                };
                Action::OpenClawDailyMemoryOpenFile {
                    filename: row.filename().to_string(),
                }
            }
            KeyCode::Char('a') => {
                self.open_daily_memory_filename_prompt(Self::today_daily_memory_filename());
                Action::None
            }
            KeyCode::Char('d') => {
                let Some(row) = visible.get(self.daily_memory_idx) else {
                    return Action::None;
                };
                let filename = row.filename().to_string();
                self.overlay = Overlay::Confirm(ConfirmOverlay {
                    title: texts::tui_openclaw_daily_memory_delete_title().to_string(),
                    message: texts::tui_openclaw_daily_memory_delete_message(&filename),
                    action: ConfirmAction::OpenClawDailyMemoryDelete { filename },
                });
                Action::None
            }
            KeyCode::Char('o') => Action::OpenClawOpenDirectory {
                subdir: "memory".to_string(),
            },
            _ => Action::None,
        }
    }

    pub(crate) fn on_hermes_memory_key(&mut self, key: KeyEvent, data: &UiData) -> Action {
        let selected = hermes_memory_kind_for_index(self.hermes_memory_idx);
        match key.code {
            KeyCode::Up => {
                self.hermes_memory_idx = self.hermes_memory_idx.saturating_sub(1);
                Action::None
            }
            KeyCode::Down => {
                self.hermes_memory_idx =
                    (self.hermes_memory_idx + 1).min(HERMES_MEMORY_ROW_COUNT - 1);
                Action::None
            }
            KeyCode::Enter | KeyCode::Char('e') => Action::HermesMemoryOpen { kind: selected },
            KeyCode::Char(' ') | KeyCode::Char('x') => Action::HermesMemorySetEnabled {
                kind: selected,
                enabled: !data.config.hermes_memory.enabled(selected),
            },
            KeyCode::Char('o') => Action::HermesOpenMemoryDirectory,
            _ => Action::None,
        }
    }

    pub(crate) fn on_config_openclaw_env_key(&mut self, key: KeyEvent, data: &UiData) -> Action {
        match key.code {
            KeyCode::Enter | KeyCode::Char('e') => {
                self.open_openclaw_editor(
                    texts::tui_openclaw_config_env_editor_title(),
                    data.config.openclaw_env.as_ref(),
                    EditorSubmit::ConfigOpenClawEnv,
                );
                Action::None
            }
            _ => Action::None,
        }
    }

    fn openclaw_tools_form(&mut self, data: &UiData) -> &mut OpenClawToolsFormState {
        self.openclaw_tools_form.get_or_insert_with(|| {
            OpenClawToolsFormState::from_snapshot(data.config.openclaw_tools.as_ref())
        })
    }

    fn submit_openclaw_tools_form(&self) -> Action {
        let Some(form) = self.openclaw_tools_form.as_ref() else {
            return Action::None;
        };

        let content =
            serde_json::to_string_pretty(&form.to_config()).unwrap_or_else(|_| "{}".to_string());
        Action::EditorSubmit {
            submit: EditorSubmit::ConfigOpenClawTools,
            content,
        }
    }

    fn try_submit_openclaw_tools_form(&mut self, data: &UiData) -> Action {
        if super::openclaw_tools_has_blocking_warning(data) {
            self.push_toast(
                texts::tui_toast_openclaw_tools_save_blocked_parse_error(),
                ToastKind::Error,
            );
            Action::None
        } else {
            self.submit_openclaw_tools_form()
        }
    }

    pub(super) fn mutate_openclaw_tools_form<F>(&mut self, data: &UiData, mutate: F) -> Action
    where
        F: FnOnce(&mut OpenClawToolsFormState),
    {
        let changed = {
            let form = self.openclaw_tools_form(data);
            let before = form.clone();
            mutate(form);
            *form != before
        };

        if changed {
            self.try_submit_openclaw_tools_form(data)
        } else {
            Action::None
        }
    }

    fn open_openclaw_tools_profile_picker(&mut self, data: &UiData) -> Action {
        let selected = super::openclaw_tools_profile_picker_index(
            self.openclaw_tools_form(data).profile.as_deref(),
        );
        self.overlay = Overlay::OpenClawToolsProfilePicker { selected };
        Action::None
    }

    pub(super) fn open_openclaw_tools_rule_editor(
        &mut self,
        section: OpenClawToolsSection,
        row: Option<usize>,
        initial: String,
    ) -> Action {
        let title = match section {
            OpenClawToolsSection::Allow => texts::tui_openclaw_tools_allow_list_label(),
            OpenClawToolsSection::Deny => texts::tui_openclaw_tools_deny_list_label(),
            OpenClawToolsSection::Profile => return Action::None,
        };

        self.overlay = Overlay::TextInput(TextInputState {
            title: title.to_string(),
            prompt: texts::tui_openclaw_tools_pattern_placeholder().to_string(),
            input: TextInput::new(initial),
            submit: TextSubmit::OpenClawToolsRule { section, row },
            secret: false,
        });
        Action::None
    }

    fn open_current_openclaw_tools_rule_editor(&mut self, data: &UiData) -> Action {
        let (section, row, initial) = {
            let form = self.openclaw_tools_form(data);
            (
                form.section,
                form.selected_rule_row(),
                form.selected_rule_value().unwrap_or_default().to_string(),
            )
        };
        self.open_openclaw_tools_rule_editor(section, row, initial)
    }

    pub(crate) fn on_config_openclaw_tools_key(&mut self, key: KeyEvent, data: &UiData) -> Action {
        if super::openclaw_tools_load_failed(data) {
            self.openclaw_tools_form = None;
            if matches!(key.code, KeyCode::Enter) {
                self.push_toast(
                    texts::tui_toast_openclaw_tools_save_blocked_parse_error(),
                    ToastKind::Error,
                );
            }
            return Action::None;
        }

        let section = self.openclaw_tools_form(data).section;
        match key.code {
            KeyCode::Up => {
                self.openclaw_tools_form(data).move_up();
                Action::None
            }
            KeyCode::Down => {
                self.openclaw_tools_form(data).move_down();
                Action::None
            }
            KeyCode::Enter | KeyCode::Char('e') => match section {
                OpenClawToolsSection::Profile => self.open_openclaw_tools_profile_picker(data),
                OpenClawToolsSection::Allow | OpenClawToolsSection::Deny => {
                    self.open_current_openclaw_tools_rule_editor(data)
                }
            },
            KeyCode::Delete | KeyCode::Backspace => {
                self.mutate_openclaw_tools_form(data, |form| form.remove_current_list_item())
            }
            _ => Action::None,
        }
    }

    fn openclaw_agents_form(&mut self, data: &UiData) -> &mut OpenClawAgentsFormState {
        self.openclaw_agents_form.get_or_insert_with(|| {
            OpenClawAgentsFormState::from_snapshot(data.config.openclaw_agents_defaults.as_ref())
        })
    }

    fn submit_openclaw_agents_form(&self) -> Action {
        let Some(form) = self.openclaw_agents_form.as_ref() else {
            return Action::None;
        };

        let content =
            serde_json::to_string_pretty(&form.to_config()).unwrap_or_else(|_| "{}".to_string());
        Action::EditorSubmit {
            submit: EditorSubmit::ConfigOpenClawAgents,
            content,
        }
    }

    fn try_submit_openclaw_agents_form(&mut self, data: &UiData) -> Action {
        let message = if super::openclaw_agents_has_blocking_warning(data) {
            Some(texts::tui_toast_openclaw_agents_save_blocked_parse_error())
        } else if self
            .openclaw_agents_form(data)
            .has_unmigratable_legacy_timeout()
        {
            Some(texts::tui_toast_openclaw_agents_save_blocked_legacy_timeout())
        } else {
            None
        };

        if let Some(message) = message {
            self.push_toast(message, ToastKind::Error);
            Action::None
        } else {
            self.submit_openclaw_agents_form()
        }
    }

    pub(super) fn mutate_openclaw_agents_form<F>(&mut self, data: &UiData, mutate: F) -> Action
    where
        F: FnOnce(&mut OpenClawAgentsFormState),
    {
        let changed = {
            let form = self.openclaw_agents_form(data);
            let before = form.clone();
            mutate(form);
            *form != before
        };

        if changed {
            self.try_submit_openclaw_agents_form(data)
        } else {
            Action::None
        }
    }

    pub(super) fn submit_openclaw_agents_runtime_popup_field(
        &mut self,
        data: &UiData,
        field: OpenClawAgentsRuntimeField,
        raw: String,
    ) -> Action {
        if raw.trim().is_empty() {
            return Action::None;
        }

        let changed = {
            let form = self.openclaw_agents_form(data);
            let before = form.runtime_field_value(field).to_string();
            if before == raw {
                false
            } else {
                form.set_runtime_field(field, raw);
                true
            }
        };

        if !changed {
            return Action::None;
        }

        if super::openclaw_agents_has_blocking_warning(data) {
            self.push_toast(
                texts::tui_toast_openclaw_agents_save_blocked_parse_error(),
                ToastKind::Error,
            );
            Action::None
        } else {
            self.submit_openclaw_agents_form()
        }
    }

    fn open_openclaw_agents_runtime_editor(&mut self, data: &UiData) -> Action {
        let (field, title, buffer) = {
            let form = self.openclaw_agents_form(data);
            let Some(field) = form.selected_runtime_field() else {
                return Action::None;
            };
            let title = match field {
                OpenClawAgentsRuntimeField::Workspace => texts::tui_openclaw_agents_workspace(),
                OpenClawAgentsRuntimeField::Timeout => texts::tui_openclaw_agents_timeout(),
                OpenClawAgentsRuntimeField::ContextTokens => {
                    texts::tui_openclaw_agents_context_tokens()
                }
                OpenClawAgentsRuntimeField::MaxConcurrent => {
                    texts::tui_openclaw_agents_max_concurrent()
                }
            };

            (field, title, form.runtime_field_value(field).to_string())
        };

        self.overlay = Overlay::TextInput(TextInputState {
            title: title.to_string(),
            prompt: title.to_string(),
            input: TextInput::new(buffer),
            submit: TextSubmit::OpenClawAgentsRuntimeField { field },
            secret: false,
        });
        Action::None
    }

    fn open_openclaw_agents_model_picker(&mut self, data: &UiData) -> Action {
        let model_options = super::openclaw_agents_model_options(data);
        let Some((insert_at, selected, options)) = ({
            let form = self.openclaw_agents_form(data);
            match form.section {
                OpenClawAgentsSection::PrimaryModel => (!model_options.is_empty()).then(|| {
                    (
                        0,
                        form.primary_model_picker_selection(&model_options),
                        model_options.clone(),
                    )
                }),
                OpenClawAgentsSection::FallbackModels => {
                    let row = form.row.min(form.fallbacks.len());
                    if row < form.fallbacks.len() {
                        let options = form.available_fallback_options_for_row(row, &model_options);
                        (!options.is_empty()).then(|| {
                            (
                                row,
                                form.current_fallback_picker_selection(row, &options),
                                options,
                            )
                        })
                    } else {
                        let options = form.available_fallback_options(&model_options);
                        (!options.is_empty()).then(|| (row, 0, options))
                    }
                }
                OpenClawAgentsSection::Runtime => None,
            }
        }) else {
            return Action::None;
        };

        self.overlay = Overlay::OpenClawAgentsFallbackPicker {
            insert_at,
            selected,
            options,
        };
        Action::None
    }

    fn skip_disabled_openclaw_agents_add_row(&mut self, data: &UiData, moving_down: bool) {
        let disabled_add_row_selected = {
            let model_options = super::openclaw_agents_model_options(data);
            let form = self.openclaw_agents_form(data);
            form.section == OpenClawAgentsSection::FallbackModels
                && form.row == form.fallbacks.len()
                && form.available_fallback_options(&model_options).is_empty()
        };

        if disabled_add_row_selected {
            let form = self.openclaw_agents_form(data);
            if moving_down {
                form.move_down();
            } else {
                form.move_up();
            }
        }
    }

    pub(crate) fn on_config_openclaw_agents_key(&mut self, key: KeyEvent, data: &UiData) -> Action {
        if super::openclaw_agents_load_failed(data) {
            self.openclaw_agents_form = None;
            if matches!(key.code, KeyCode::Enter) {
                self.push_toast(
                    texts::tui_toast_openclaw_agents_save_blocked_parse_error(),
                    ToastKind::Error,
                );
            }
            return Action::None;
        }
        let section = self.openclaw_agents_form(data).section;

        match key.code {
            KeyCode::Up => {
                self.openclaw_agents_form(data).move_up();
                self.skip_disabled_openclaw_agents_add_row(data, false);
                Action::None
            }
            KeyCode::Down => {
                self.openclaw_agents_form(data).move_down();
                self.skip_disabled_openclaw_agents_add_row(data, true);
                Action::None
            }
            KeyCode::Enter => match section {
                OpenClawAgentsSection::PrimaryModel | OpenClawAgentsSection::FallbackModels => {
                    self.open_openclaw_agents_model_picker(data)
                }
                OpenClawAgentsSection::Runtime => self.open_openclaw_agents_runtime_editor(data),
            },
            KeyCode::Delete | KeyCode::Backspace => match section {
                OpenClawAgentsSection::PrimaryModel => {
                    self.mutate_openclaw_agents_form(data, |form| form.clear_primary_model())
                }
                OpenClawAgentsSection::FallbackModels => {
                    self.mutate_openclaw_agents_form(data, |form| form.remove_current_fallback())
                }
                OpenClawAgentsSection::Runtime => self.mutate_openclaw_agents_form(data, |form| {
                    if let Some(field) = form.selected_runtime_field() {
                        form.clear_runtime_field(field);
                    }
                }),
            },
            _ => Action::None,
        }
    }

    pub(crate) fn on_config_webdav_key(&mut self, key: KeyEvent, data: &UiData) -> Action {
        let items = visible_webdav_config_items(&self.filter);
        match key.code {
            KeyCode::Up => {
                self.config_webdav_idx = self.config_webdav_idx.saturating_sub(1);
                Action::None
            }
            KeyCode::Down => {
                if !items.is_empty() {
                    self.config_webdav_idx = (self.config_webdav_idx + 1).min(items.len() - 1);
                }
                Action::None
            }
            KeyCode::Char('e') => {
                let Some(item) = items.get(self.config_webdav_idx) else {
                    return Action::None;
                };
                if matches!(item, WebDavConfigItem::Settings) {
                    let webdav_json = match data.config.webdav_sync.as_ref() {
                        Some(cfg) => {
                            serde_json::to_string_pretty(cfg).unwrap_or_else(|_| "{}".to_string())
                        }
                        None => serde_json::to_string_pretty(
                            &crate::settings::WebDavSyncSettings::default(),
                        )
                        .unwrap_or_else(|_| "{}".to_string()),
                    };
                    self.open_editor(
                        texts::tui_webdav_settings_editor_title(),
                        EditorKind::Json,
                        webdav_json,
                        EditorSubmit::ConfigWebDavSettings,
                    );
                }
                Action::None
            }
            KeyCode::Enter => {
                let Some(item) = items.get(self.config_webdav_idx) else {
                    return Action::None;
                };
                match item {
                    WebDavConfigItem::Settings => {
                        let webdav_json = match data.config.webdav_sync.as_ref() {
                            Some(cfg) => serde_json::to_string_pretty(cfg)
                                .unwrap_or_else(|_| "{}".to_string()),
                            None => serde_json::to_string_pretty(
                                &crate::settings::WebDavSyncSettings::default(),
                            )
                            .unwrap_or_else(|_| "{}".to_string()),
                        };
                        self.open_editor(
                            texts::tui_webdav_settings_editor_title(),
                            EditorKind::Json,
                            webdav_json,
                            EditorSubmit::ConfigWebDavSettings,
                        );
                        Action::None
                    }
                    WebDavConfigItem::CheckConnection => Action::ConfigWebDavCheckConnection,
                    WebDavConfigItem::Upload => Action::ConfigWebDavUpload,
                    WebDavConfigItem::Download => Action::ConfigWebDavDownload,
                    WebDavConfigItem::Reset => Action::ConfigWebDavReset,
                    WebDavConfigItem::JianguoyunQuickSetup => {
                        self.webdav_quick_setup_username = None;
                        self.overlay = Overlay::TextInput(TextInputState {
                            title: texts::tui_webdav_jianguoyun_setup_title().to_string(),
                            prompt: texts::tui_webdav_jianguoyun_username_prompt().to_string(),
                            input: TextInput::new(""),
                            submit: TextSubmit::WebDavJianguoyunUsername,
                            secret: false,
                        });
                        Action::None
                    }
                }
            }
            _ => Action::None,
        }
    }

    pub(crate) fn on_settings_key(&mut self, key: KeyEvent, _data: &UiData) -> Action {
        let settings_len = SettingsItem::ALL.len();
        match key.code {
            KeyCode::Up => {
                self.settings_idx = self.settings_idx.saturating_sub(1);
                Action::None
            }
            KeyCode::Down => {
                self.settings_idx = (self.settings_idx + 1).min(settings_len - 1);
                Action::None
            }
            KeyCode::Enter => match SettingsItem::ALL.get(self.settings_idx) {
                Some(SettingsItem::Language) => {
                    let next = match current_language() {
                        Language::English => Language::Chinese,
                        Language::Chinese => Language::English,
                    };
                    Action::SetLanguage(next)
                }
                Some(SettingsItem::VisibleAppsMode) => {
                    let current = crate::settings::get_visible_apps_settings().mode;
                    let next = match current {
                        crate::settings::VisibleAppsMode::Auto => {
                            crate::settings::VisibleAppsMode::Manual
                        }
                        crate::settings::VisibleAppsMode::Manual => {
                            crate::settings::VisibleAppsMode::Auto
                        }
                    };
                    Action::SetVisibleAppsMode { mode: next }
                }
                Some(SettingsItem::VisibleApps) => {
                    self.overlay = Overlay::VisibleAppsPicker {
                        selected: app_type_picker_index(&self.app_type),
                        apps: crate::settings::get_visible_apps(),
                    };
                    Action::None
                }
                Some(SettingsItem::OpenClawConfigDir) => {
                    let buffer = crate::settings::get_settings()
                        .openclaw_config_dir
                        .unwrap_or_default();
                    self.overlay = Overlay::TextInput(TextInputState {
                        title: texts::tui_settings_openclaw_config_dir_label().to_string(),
                        prompt: texts::tui_settings_openclaw_config_dir_prompt().to_string(),
                        input: TextInput::new(buffer),
                        submit: TextSubmit::SettingsOpenClawConfigDir,
                        secret: false,
                    });
                    Action::None
                }
                Some(SettingsItem::SkipClaudeOnboarding) => {
                    let current = crate::settings::get_skip_claude_onboarding();
                    let next = !current;
                    let path = crate::config::get_claude_mcp_path();

                    self.overlay = Overlay::Confirm(ConfirmOverlay {
                        title: texts::tui_confirm_title().to_string(),
                        message: texts::skip_claude_onboarding_confirm(
                            next,
                            path.to_string_lossy().as_ref(),
                        ),
                        action: ConfirmAction::SettingsSetSkipClaudeOnboarding { enabled: next },
                    });
                    Action::None
                }
                Some(SettingsItem::ClaudePluginIntegration) => {
                    let current = crate::settings::get_enable_claude_plugin_integration();
                    let next = !current;
                    let path = match crate::claude_plugin::claude_config_path() {
                        Ok(path) => path,
                        Err(_) => std::path::PathBuf::from("~/.claude/config.json"),
                    };

                    self.overlay = Overlay::Confirm(ConfirmOverlay {
                        title: texts::tui_confirm_title().to_string(),
                        message: texts::enable_claude_plugin_integration_confirm(
                            next,
                            path.to_string_lossy().as_ref(),
                        ),
                        action: ConfirmAction::SettingsSetClaudePluginIntegration { enabled: next },
                    });
                    Action::None
                }
                Some(SettingsItem::Proxy) => self.push_route_and_switch(Route::SettingsProxy),
                Some(SettingsItem::CheckForUpdates) => Action::CheckUpdate,
                None => Action::None,
            },
            _ => Action::None,
        }
    }

    pub(crate) fn request_auto_failover_toggle(&mut self, data: &UiData) -> Action {
        if !supports_failover_controls(&self.app_type) {
            return Action::None;
        }

        let enabled = !data.proxy.auto_failover_enabled;
        if !enabled {
            return Action::SetProxyAutoFailover {
                app_type: self.app_type.clone(),
                enabled,
            };
        }

        let queue_empty = !data
            .providers
            .rows
            .iter()
            .any(|row| row.provider.in_failover_queue);
        if queue_empty {
            self.push_toast(
                crate::cli::failover_policy::auto_failover_queue_empty_message(),
                ToastKind::Warning,
            );
            return Action::None;
        }

        if data
            .proxy
            .routes_current_app_through_proxy(&self.app_type)
            .is_some_and(|active| !active)
        {
            self.overlay = Overlay::Confirm(ConfirmOverlay {
                title: texts::tui_confirm_title().to_string(),
                message: crate::t!(
                    "Automatic failover requires proxy routing for this app. Enable proxy takeover for the app first?",
                    "故障转移需要当前应用走代理才能生效。是否同时开启当前应用代理并启用故障转移？"
                )
                .to_string(),
                action: ConfirmAction::ProxyEnableAndAutoFailover {
                    app_type: self.app_type.clone(),
                },
            });
            return Action::None;
        }

        Action::SetProxyAutoFailover {
            app_type: self.app_type.clone(),
            enabled,
        }
    }

    pub(crate) fn on_settings_proxy_key(&mut self, key: KeyEvent, data: &UiData) -> Action {
        let items_len = LocalProxySettingsItem::ALL.len();
        match key.code {
            KeyCode::Up => {
                self.settings_proxy_idx = self.settings_proxy_idx.saturating_sub(1);
                Action::None
            }
            KeyCode::Down => {
                self.settings_proxy_idx = (self.settings_proxy_idx + 1).min(items_len - 1);
                Action::None
            }
            KeyCode::Enter => match LocalProxySettingsItem::ALL.get(self.settings_proxy_idx) {
                Some(LocalProxySettingsItem::AutoFailover) => {
                    self.request_auto_failover_toggle(data)
                }
                Some(LocalProxySettingsItem::ListenAddress) => {
                    if data.proxy.running {
                        self.push_toast(
                            texts::tui_toast_proxy_settings_stop_before_edit(),
                            ToastKind::Info,
                        );
                        return Action::None;
                    }
                    self.overlay = Overlay::TextInput(TextInputState {
                        title: texts::tui_settings_proxy_title().to_string(),
                        prompt: texts::tui_settings_proxy_listen_address_prompt().to_string(),
                        input: TextInput::new(data.proxy.configured_listen_address.clone()),
                        submit: TextSubmit::SettingsProxyListenAddress,
                        secret: false,
                    });
                    Action::None
                }
                Some(LocalProxySettingsItem::ListenPort) => {
                    if data.proxy.running {
                        self.push_toast(
                            texts::tui_toast_proxy_settings_stop_before_edit(),
                            ToastKind::Info,
                        );
                        return Action::None;
                    }
                    self.overlay = Overlay::TextInput(TextInputState {
                        title: texts::tui_settings_proxy_title().to_string(),
                        prompt: texts::tui_settings_proxy_listen_port_prompt().to_string(),
                        input: TextInput::new(data.proxy.configured_listen_port.to_string()),
                        submit: TextSubmit::SettingsProxyListenPort,
                        secret: false,
                    });
                    Action::None
                }
                None => Action::None,
            },
            _ => Action::None,
        }
    }
    pub fn open_editor(
        &mut self,
        title: impl Into<String>,
        kind: EditorKind,
        initial: impl Into<String>,
        submit: EditorSubmit,
    ) {
        self.filter.active = false;
        self.overlay = Overlay::None;
        self.focus = Focus::Content;
        self.editor = Some(EditorState::new(title, kind, submit, initial));
    }

    pub(crate) fn common_snippet_text_for(&self, app_type: &AppType, data: &UiData) -> String {
        if app_type == &self.app_type {
            data.config.common_snippet.clone()
        } else {
            data.config
                .common_snippets
                .get(app_type)
                .cloned()
                .unwrap_or_default()
        }
    }

    pub(crate) fn open_proxy_help_view(
        &mut self,
        data: &UiData,
        config: Option<&crate::proxy::ProxyConfig>,
    ) {
        let current_provider = if data.providers.current_id.trim().is_empty() {
            crate::t!("(not set)", "（未设置）").to_string()
        } else {
            data.providers.current_id.clone()
        };

        let runtime_state = if data.proxy.running {
            crate::t!("running", "运行中")
        } else {
            crate::t!("stopped", "未运行")
        };
        let current_route = data.proxy.routes_current_app_through_proxy(&self.app_type);
        let proxy_action_available = current_route.is_some_and(|current_route| {
            !data.proxy.running || data.proxy.managed_runtime || current_route
        });
        let route_state = match current_route {
            Some(true) => crate::t!("enabled", "开启"),
            Some(false) => crate::t!("disabled", "关闭"),
            None => crate::t!("not supported", "不支持"),
        };
        let toggle_action = if current_route.is_some() && proxy_action_available {
            Some(TextViewAction::ProxyToggleManagedRoute)
        } else {
            None
        };

        let mut lines = vec![
            crate::t!(
                "Managed proxy routing for the current app.",
                "当前应用的托管代理路由状态。"
            )
            .to_string(),
            String::new(),
            format!(
                "{}: {}",
                crate::t!("Current app", "当前应用"),
                self.app_type.as_str()
            ),
            format!(
                "{}: {}",
                crate::t!("Current provider", "当前供应商"),
                current_provider
            ),
            format!("{}: {}", crate::t!("Runtime", "运行态"), runtime_state),
            format!(
                "{}: {}",
                crate::t!("Current app route", "当前应用路由"),
                route_state
            ),
            crate::t!(
                "Proxy routes are started and stopped by the cc-switch daemon.",
                "代理路由由 cc-switch daemon 启停。"
            )
            .to_string(),
        ];

        if let Some(config) = config {
            lines.extend([
                format!(
                    "{}: {}:{}",
                    crate::t!("Listen", "监听"),
                    config.listen_address,
                    config.listen_port
                ),
                format!(
                    "{}: {}",
                    crate::t!("Global proxy switch", "全局代理开关"),
                    if data.proxy.enabled {
                        crate::t!("enabled", "开启")
                    } else {
                        crate::t!("disabled", "关闭")
                    }
                ),
            ]);
        } else {
            lines.push(
                crate::t!(
                    "Proxy configuration is unavailable.",
                    "代理配置暂时不可用。"
                )
                .to_string(),
            );
        }

        lines.push(String::new());
        lines.push(match current_route {
            Some(true) => crate::t!(
                "Press T to restore the current app to its live config.",
                "按 T 恢复当前应用的 live 配置。"
            )
            .to_string(),
            Some(false) if !proxy_action_available => {
                texts::tui_proxy_dashboard_running_elsewhere().to_string()
            }
            Some(false) if data.proxy.running => crate::t!(
                "Press T to route the current app through the running managed proxy.",
                "按 T 让当前应用接入正在运行的托管代理。"
            )
            .to_string(),
            Some(false) => crate::t!(
                "Press T to start the managed proxy and route the current app through cc-switch.",
                "按 T 启动托管代理，并让当前应用走 cc-switch。"
            )
            .to_string(),
            None => crate::t!(
                "This app does not support managed proxy routing in the TUI.",
                "这个应用暂不支持在 TUI 中使用托管代理路由。"
            )
            .to_string(),
        });

        if matches!(self.app_type, AppType::Claude) {
            lines.push(String::new());
            lines.push(crate::t!("Manual Claude setup:", "Claude 手动接线：").to_string());
            if let Some(config) = config {
                lines.push(format!(
                    "{}: cc-switch proxy serve --listen-address {} --listen-port {}",
                    crate::t!("Foreground command", "前台命令"),
                    config.listen_address,
                    config.listen_port
                ));
                lines.push(format!(
                    "ANTHROPIC_BASE_URL=http://{}:{}",
                    config.listen_address, config.listen_port
                ));
            } else {
                lines.push(format!(
                    "{}: cc-switch proxy serve",
                    crate::t!("Foreground command", "前台命令")
                ));
                lines.push("ANTHROPIC_BASE_URL=http://127.0.0.1:3456".to_string());
            }
            lines.extend([
                "ANTHROPIC_AUTH_TOKEN=proxy-placeholder".to_string(),
                crate::t!(
                    "Keep the real upstream base URL and key in the selected Claude provider inside cc-switch.",
                    "真实上游地址和密钥仍保存在 cc-switch 里当前选中的 Claude provider。"
                )
                .to_string(),
            ]);
        }

        self.overlay = Overlay::TextView(TextViewState {
            title: texts::tui_config_item_proxy().to_string(),
            lines,
            scroll: 0,
            action: toggle_action,
        });
    }

    pub(crate) fn open_common_snippet_editor(
        &mut self,
        app_type: AppType,
        data: &UiData,
        initial_override: Option<String>,
        source: CommonSnippetViewSource,
    ) {
        let snippet = initial_override.unwrap_or_else(|| {
            let snippet = self.common_snippet_text_for(&app_type, data);
            if snippet.trim().is_empty() {
                texts::tui_default_common_snippet_for_app(app_type.as_str()).to_string()
            } else {
                snippet
            }
        });

        let kind = if matches!(app_type, AppType::Codex) {
            EditorKind::Toml
        } else {
            EditorKind::Json
        };

        self.open_editor(
            texts::tui_common_snippet_title(app_type.as_str()),
            kind,
            snippet,
            EditorSubmit::ConfigCommonSnippet { app_type, source },
        );
    }

    fn maybe_show_common_config_notice(&mut self) {
        if self.common_config_notice_confirmed
            || !ProviderAddFormState::supports_common_config(&self.app_type)
        {
            return;
        }

        self.overlay = Overlay::Confirm(ConfirmOverlay {
            title: texts::tui_common_config_notice_title().to_string(),
            message: texts::tui_common_config_notice_message(self.app_type.as_str()),
            action: ConfirmAction::CommonConfigNotice,
        });
    }

    pub(crate) fn open_provider_add_form(&mut self, data: &UiData) {
        self.filter.active = false;
        self.overlay = Overlay::None;
        self.focus = Focus::Content;
        self.editor = None;
        self.form = Some(FormState::ProviderAdd(
            ProviderAddFormState::new_with_common_snippet(
                self.app_type.clone(),
                &data.config.common_snippet,
            ),
        ));
        self.maybe_show_common_config_notice();
    }

    pub(crate) fn open_provider_edit_form(
        &mut self,
        row: &super::data::ProviderRow,
        data: &UiData,
    ) {
        self.filter.active = false;
        self.overlay = Overlay::None;
        self.focus = Focus::Content;
        self.editor = None;
        self.form = Some(FormState::ProviderAdd(
            ProviderAddFormState::from_provider_with_common_snippet(
                self.app_type.clone(),
                &row.provider,
                &data.config.common_snippet,
            ),
        ));
        self.maybe_show_common_config_notice();
    }

    pub(crate) fn open_mcp_add_form(&mut self) {
        self.filter.active = false;
        self.overlay = Overlay::None;
        self.focus = Focus::Content;
        self.editor = None;
        let mut state = McpAddFormState::new();
        state.apps.set_enabled_for(&self.app_type, true);
        state.rebase_initial_snapshot();
        self.form = Some(FormState::McpAdd(state));
    }

    pub(crate) fn open_mcp_edit_form(&mut self, row: &super::data::McpRow) {
        self.filter.active = false;
        self.overlay = Overlay::None;
        self.focus = Focus::Content;
        self.editor = None;
        self.form = Some(FormState::McpAdd(McpAddFormState::from_server(&row.server)));
    }

    pub(crate) fn open_prompt_create_form(&mut self, data: &UiData) {
        self.filter.active = false;
        self.editor = None;
        self.overlay = Overlay::None;
        let name = format!("Prompt {}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
        let existing_ids = data
            .prompts
            .rows
            .iter()
            .map(|row| row.id.clone())
            .collect::<Vec<_>>();
        let id = crate::services::PromptService::generate_prompt_id(&name, &existing_ids);
        self.form = Some(FormState::PromptMeta(PromptMetaFormState::new(id, name)));
        self.focus = Focus::Content;
    }
}

pub(crate) fn hermes_memory_kind_for_index(index: usize) -> crate::hermes_config::MemoryKind {
    match index.min(HERMES_MEMORY_ROW_COUNT - 1) {
        1 => crate::hermes_config::MemoryKind::User,
        _ => crate::hermes_config::MemoryKind::Memory,
    }
}
