//! Entry detail dialog for editing complex map entries
//!
//! Provides a modal dialog for editing complex map entries using the same
//! SettingItem/SettingControl infrastructure as the main settings UI.

use super::items::{
    build_item_from_value, control_to_value, ItemBoxStyle, SettingControl, SettingItem,
};
use super::schema::{SettingSchema, SettingType};
use crate::view::controls::{FocusState, TextInputState};
use rust_i18n::t;
use serde_json::Value;
use std::collections::{HashMap, HashSet};

/// A per-field action affordance rendered at the right edge of a field's row.
///
/// These target different values:
/// * `Reset` sets the field to its *built-in default* (the value the bundled
///   config ships for this entry).
/// * `Inherit` sets the field to `null`. It renders as `[Inherit]` when null
///   falls back to a parent-scope value (e.g. a per-language `line_wrap`
///   inheriting `editor.line_wrap`), or `[Clear]` when there's no such fallback
///   and null just unsets the field (e.g. a `formatter`). See
///   [`EntryDialogState::field_action_buttons`].
///
/// A field only offers the action(s) that lead to a *different* result, so a
/// nullable field whose built-in default is itself `null` shows only the
/// Inherit/Clear button (Reset would be identical), while a plain field with a
/// built-in default and no inheritance chain shows only `Reset`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldAction {
    /// Set the field to its built-in default value.
    Reset,
    /// Set the field to `null` — inherit a parent value, or clear it when there
    /// is no parent to inherit from.
    Inherit,
}

/// Simple scalar controls whose per-field action buttons join the Tab order.
/// Composite controls (lists, maps, JSON) keep their own internal navigation,
/// so their inherit affordance stays mouse-only.
fn is_simple_field_control(control: &SettingControl) -> bool {
    matches!(
        control,
        SettingControl::Toggle(_)
            | SettingControl::Number(_)
            | SettingControl::Text(_)
            | SettingControl::Dropdown(_)
    )
}

/// Lay out right-aligned per-field action buttons against `right_edge`
/// (exclusive). Returns `(action, x, width)` left to right, with a one-column
/// gap between buttons and a one-column margin at the right edge. Shared by the
/// renderer and the click hit-tester so their geometry can't drift.
pub fn layout_field_action_buttons(
    buttons: &[(FieldAction, String)],
    right_edge: u16,
) -> Vec<(FieldAction, u16, u16)> {
    if buttons.is_empty() {
        return Vec::new();
    }
    let widths: Vec<u16> = buttons
        .iter()
        .map(|(_, label)| label.chars().count() as u16)
        .collect();
    let gaps = buttons.len().saturating_sub(1) as u16;
    let total: u16 = widths.iter().sum::<u16>() + gaps + 1;
    let mut x = right_edge.saturating_sub(total);
    let mut out = Vec::with_capacity(buttons.len());
    for ((action, _), w) in buttons.iter().zip(widths) {
        out.push((*action, x, w));
        x = x.saturating_add(w + 1);
    }
    out
}

/// State for the entry detail dialog
#[derive(Debug, Clone)]
pub struct EntryDialogState {
    /// The entry key (e.g., "rust" for language)
    pub entry_key: String,
    /// The map path this entry belongs to (e.g., "/languages", "/lsp")
    pub map_path: String,
    /// Human-readable title for the dialog
    pub title: String,
    /// Whether this is a new entry (vs editing existing)
    pub is_new: bool,
    /// Items in the dialog (using same SettingItem structure as main settings)
    pub items: Vec<SettingItem>,
    /// Currently selected item index
    pub selected_item: usize,
    /// Sub-focus index within the selected item (for TextList/Map navigation)
    pub sub_focus: Option<usize>,
    /// Whether we're in text editing mode
    pub editing_text: bool,
    /// Currently focused button (0=Save, 1=Delete, 2=Cancel for existing; 0=Save, 1=Cancel for new)
    pub focused_button: usize,
    /// Whether focus is on buttons (true) or items (false)
    pub focus_on_buttons: bool,
    /// Whether deletion was requested
    pub delete_requested: bool,
    /// Scroll offset for the items area
    pub scroll_offset: usize,
    /// Last known viewport height (updated during render)
    pub viewport_height: usize,
    /// Hovered item index (for mouse hover feedback)
    pub hover_item: Option<usize>,
    /// Hovered button index (for mouse hover feedback)
    pub hover_button: Option<usize>,
    /// Original value when dialog was opened (for Cancel to restore)
    pub original_value: Value,
    /// Index of first editable item (items before this are read-only)
    /// Used for rendering separator and focus navigation
    pub first_editable_index: usize,
    /// Whether deletion is disabled (for auto-managed entries like plugins)
    pub no_delete: bool,
    /// When true, the dialog wraps a single non-Object value (e.g., an ObjectArray).
    /// `to_value()` returns the raw control value instead of wrapping in an Object.
    pub is_single_value: bool,
    /// True when the dialog edits an item in an array (constructed via
    /// `for_array_item`); false for map entries (`from_schema`). Drives
    /// the Delete button's label/confirmation copy so the prompt doesn't
    /// show a numeric index as if it were a meaningful name.
    pub is_array_item: bool,
    /// Set to true on the first user-driven mutation (typed char,
    /// toggled bool, list add/remove, etc.). Drives the dirty
    /// indicator + the Esc discard prompt without relying on a
    /// JSON-equality check that's too noisy at the schema layer.
    pub user_edited: bool,
    /// When `Some(i)`, keyboard focus is on the i-th per-field action button
    /// (`[Reset]`/`[Inherit]`/`[Clear]`) of the currently selected field rather
    /// than on the field's control — `i` indexes [`field_action_buttons`]. Tab
    /// moves onto these buttons and Enter/Space activates them; it is the only
    /// keyboard path to the per-field actions.
    pub field_button_focus: Option<usize>,
    /// Field names (item path without the leading `/`) that genuinely *inherit*
    /// from a parent scope when unset — e.g. a per-language `line_wrap` falls
    /// back to the global `editor.line_wrap`. For these the per-field "set to
    /// null" button is labelled `[Inherit]`; for everything else (a `formatter`
    /// with no global fallback) it's labelled `[Clear]`, since null just unsets
    /// the value rather than inheriting one. Empty means "nothing inherits".
    pub inheritable_fields: HashSet<String>,
    /// Snapshot of the field being text-edited, captured the moment editing
    /// begins (before `start_editing` mutates the control). Esc restores it so
    /// an in-progress edit is discarded — the platform convention where Enter
    /// and Tab commit a field while Esc cancels it. `None` whenever no edit is
    /// in flight.
    edit_snapshot: Option<FieldEditSnapshot>,
}

/// Pre-edit state of a single dialog field, used to revert an abandoned edit.
/// Cloning the whole control covers every editable kind uniformly (Text,
/// Number, TextList, Json) — each restores its exact text/value, cursor, and
/// buffers. `is_null` and the dialog-level `user_edited` flag are captured too
/// so reverting also undoes the "field is now set / dialog is dirty" side
/// effects that typing triggered via `mark_field_edited`.
#[derive(Debug, Clone)]
struct FieldEditSnapshot {
    item_index: usize,
    control: SettingControl,
    is_null: bool,
    user_edited: bool,
}

impl EntryDialogState {
    /// Create a dialog from a schema definition
    ///
    /// This is the primary, schema-driven constructor. It builds items
    /// dynamically from the SettingSchema's properties using the same
    /// build logic as the main settings UI.
    pub fn from_schema(
        key: String,
        value: &Value,
        schema: &SettingSchema,
        map_path: &str,
        is_new: bool,
        no_delete: bool,
        available_status_bar_tokens: &HashMap<String, String>,
    ) -> Self {
        let mut items = Vec::new();

        // Add key field as first item (read-only for existing entries)
        let key_item = SettingItem {
            path: "__key__".to_string(),
            name: "Key".to_string(),
            description: Some("unique identifier for this entry".to_string()),
            control: SettingControl::Text(TextInputState::new("Key").with_value(&key)),
            default: None,
            modified: false,
            layer_source: crate::config_io::ConfigLayer::System,
            read_only: !is_new, // Key is editable only for new entries
            is_auto_managed: false,
            nullable: false,
            is_null: false,
            section: None,
            is_section_start: false,
            style: ItemBoxStyle::default(),
            dual_list_sibling: None,
        };
        items.push(key_item);

        // Add schema-driven items from object properties
        let is_single_value = !matches!(&schema.setting_type, SettingType::Object { .. });
        if let SettingType::Object { properties } = &schema.setting_type {
            for prop in properties {
                let field_name = prop.path.trim_start_matches('/');
                let field_value = value.get(field_name);
                let item = build_item_from_value(prop, field_value, available_status_bar_tokens);
                items.push(item);
            }
        } else {
            // For non-object types (e.g., ObjectArray, Map), build a single item
            // from the entire value so the dialog can render it
            let item = build_item_from_value(schema, Some(value), available_status_bar_tokens);
            items.push(item);
        }

        // Sort items: read-only first, then editable (stable sort preserves x-order)
        items.sort_by_key(|item| !item.read_only);

        // Compute is_section_start for section headers in entry dialogs
        Self::compute_section_starts(&mut items);

        // Find the first editable item index
        let first_editable_index = items
            .iter()
            .position(|item| !item.read_only)
            .unwrap_or(items.len());

        // If all items are read-only, start with focus on buttons
        let focus_on_buttons = first_editable_index >= items.len();
        let selected_item = if focus_on_buttons {
            0
        } else {
            first_editable_index
        };

        let title = if is_new {
            format!("Add {}", schema.name)
        } else {
            format!("Edit {}", schema.name)
        };

        let mut result = Self {
            entry_key: key,
            map_path: map_path.to_string(),
            title,
            is_new,
            items,
            selected_item,
            sub_focus: None,
            editing_text: false,
            focused_button: 0,
            focus_on_buttons,
            delete_requested: false,
            scroll_offset: 0,
            viewport_height: 20, // Default, updated during render
            hover_item: None,
            hover_button: None,
            original_value: value.clone(),
            first_editable_index,
            no_delete,
            is_single_value,
            is_array_item: false,
            user_edited: false,
            field_button_focus: None,
            inheritable_fields: HashSet::new(),
            edit_snapshot: None,
        };
        // Pre-focus the first item in any ObjectArray controls so pressing
        // Enter opens the item editor instead of "Add new".
        result.init_object_array_focus();
        result
    }

    /// Create a dialog for an array item (no key field)
    ///
    /// Used for ObjectArray controls where items are identified by index, not key.
    pub fn for_array_item(
        index: Option<usize>,
        value: &Value,
        schema: &SettingSchema,
        array_path: &str,
        is_new: bool,
        available_status_bar_tokens: &HashMap<String, String>,
    ) -> Self {
        let mut items = Vec::new();

        // Add schema-driven items from object properties (no key field for arrays)
        if let SettingType::Object { properties } = &schema.setting_type {
            for prop in properties {
                let field_name = prop.path.trim_start_matches('/');
                let field_value = value.get(field_name);
                let item = build_item_from_value(prop, field_value, available_status_bar_tokens);
                items.push(item);
            }
        }

        // Sort items: read-only first, then editable
        items.sort_by_key(|item| !item.read_only);

        // Compute is_section_start for section headers
        Self::compute_section_starts(&mut items);

        // Find the first editable item index
        let first_editable_index = items
            .iter()
            .position(|item| !item.read_only)
            .unwrap_or(items.len());

        // If all items are read-only, start with focus on buttons
        let focus_on_buttons = first_editable_index >= items.len();
        let selected_item = if focus_on_buttons {
            0
        } else {
            first_editable_index
        };

        let title = if is_new {
            format!("Add {}", schema.name)
        } else {
            format!("Edit {}", schema.name)
        };

        Self {
            entry_key: index.map_or(String::new(), |i| i.to_string()),
            map_path: array_path.to_string(),
            title,
            is_new,
            items,
            selected_item,
            sub_focus: None,
            editing_text: false,
            focused_button: 0,
            focus_on_buttons,
            delete_requested: false,
            scroll_offset: 0,
            viewport_height: 20,
            hover_item: None,
            hover_button: None,
            original_value: value.clone(),
            first_editable_index,
            no_delete: false, // Arrays typically allow deletion
            is_single_value: false,
            is_array_item: true,
            user_edited: false,
            field_button_focus: None,
            inheritable_fields: HashSet::new(),
            edit_snapshot: None,
        }
    }

    /// Compute is_section_start flags for section headers.
    /// Marks the first item in each new section so the renderer can draw headers.
    fn compute_section_starts(items: &mut [SettingItem]) {
        let mut last_section: Option<&str> = None;
        for item in items.iter_mut() {
            let current = item.section.as_deref();
            if current.is_some() && current != last_section {
                item.is_section_start = true;
            }
            if current.is_some() {
                last_section = current;
            }
        }
    }

    /// Get the current key value from the key item
    pub fn get_key(&self) -> String {
        // Find the key item by path (may not be first after sorting)
        for item in &self.items {
            if item.path == "__key__" {
                if let SettingControl::Text(state) = &item.control {
                    return state.value();
                }
            }
        }
        self.entry_key.clone()
    }

    /// Full JSON pointer path to the entry this dialog edits.
    ///
    /// For an existing map entry under `/universal_lsp` with key `quicklsp`,
    /// this returns `/universal_lsp/quicklsp`. For array items, `entry_key`
    /// is the stringified index. For brand-new map entries whose key has
    /// not been chosen yet, this falls back to `map_path` (the parent
    /// container) — callers are expected to avoid writing at that path.
    ///
    /// Nested dialogs and any pending-change paths derived from this dialog
    /// must be rooted here — not at `map_path` — otherwise the entry key
    /// segment is dropped and changes land under `""` in the saved config.
    pub fn entry_path(&self) -> String {
        // Use the live key field so new entries pick up whatever the user
        // has typed before opening a nested dialog. For existing entries
        // the key field is read-only and equals `entry_key`, so this is
        // consistent with the on-disk path.
        let key = self.get_key();
        if key.is_empty() {
            self.map_path.clone()
        } else {
            format!("{}/{}", self.map_path, key)
        }
    }

    /// Get button count (3 for existing entries with Delete, 2 for new/no_delete entries)
    pub fn button_count(&self) -> usize {
        if self.is_new || self.no_delete {
            2 // Save, Cancel (no Delete for new entries or when no_delete is set)
        } else {
            3
        }
    }

    /// True when the user has made *any* change to the dialog since
    /// it was opened. Tracked as an explicit flag (`user_edited`)
    /// rather than comparing `to_value() != original_value`, because
    /// the rebuilt JSON shape can differ from the input shape by
    /// schema-default normalization (e.g. an absent optional field
    /// rebuilds as an explicit empty string) — which would make the
    /// dialog read as dirty at open, with no user input.
    ///
    /// Used to gate the Esc 'Discard changes?' prompt and to drive
    /// the title-bar modified indicator.
    pub fn is_dirty(&self) -> bool {
        self.user_edited
    }

    /// Mark the dialog as edited. Called from every mutator path
    /// (insert_char, toggle_bool, list add/remove, etc.) — anywhere
    /// the user can produce a change the dialog should remember.
    pub fn mark_edited(&mut self) {
        self.user_edited = true;
    }

    /// Mark the *focused field* as explicitly edited: flags the dialog dirty
    /// and clears the field's inherited state. Once the user gives a field a
    /// value of their own it is no longer inherited, so `to_value` persists it
    /// and the row shows a definite value rather than the neutral inherited
    /// chip. Call this from value-changing mutators (not cursor moves).
    fn mark_field_edited(&mut self) {
        self.user_edited = true;
        if let Some(item) = self.current_item_mut() {
            item.is_null = false;
            if let SettingControl::Toggle(state) = &mut item.control {
                state.inherited = false;
            }
        }
    }

    /// Reset the field at `idx` to *inherited* (unset). The value falls back to
    /// the global/default layer again: the row renders the neutral inherited
    /// chip / `(Inherited)` badge and `to_value` omits the field. Returns true
    /// if anything changed. No-op for read-only, non-nullable, or
    /// already-inherited fields.
    pub fn inherit_field(&mut self, idx: usize) -> bool {
        let Some(item) = self.items.get_mut(idx) else {
            return false;
        };
        if item.read_only || !item.nullable || item.is_null {
            return false;
        }
        item.is_null = true;
        item.modified = false;
        if let SettingControl::Toggle(state) = &mut item.control {
            state.inherited = true;
        }
        self.user_edited = true;
        true
    }

    /// Reset the field at `idx` to its built-in default value. Returns true if
    /// anything changed. No-op unless `reset_distinct_default` reports a
    /// distinct, non-inherited default to reset to.
    pub fn reset_field(&mut self, idx: usize) -> bool {
        let Some(default) = self.reset_distinct_default(idx) else {
            return false;
        };
        let Some(item) = self.items.get_mut(idx) else {
            return false;
        };
        super::state::update_control_from_value(&mut item.control, &default);
        // An explicit default value is a real (non-inherited) value.
        item.is_null = false;
        item.modified = false;
        if let SettingControl::Toggle(state) = &mut item.control {
            state.inherited = false;
        }
        self.user_edited = true;
        true
    }

    /// The value `[Reset]` would set, when reset is a *distinct, meaningful*
    /// action for the field at `idx` — i.e. a simple, editable field that
    /// currently overrides its built-in default, where that default isn't just
    /// `null` (which `[Inherit]` already covers). Returns `None` otherwise.
    fn reset_distinct_default(&self, idx: usize) -> Option<Value> {
        let item = self.items.get(idx)?;
        // Reset is offered for simple scalar controls and for object/JSON
        // controls (e.g. a language's `formatter`), whose only other per-field
        // action — Inherit → null — would *clear* a non-null built-in default
        // rather than restore it. Composite list/map controls and opaque
        // Complex controls are excluded.
        let resettable = is_simple_field_control(&item.control)
            || matches!(item.control, SettingControl::Json(_));
        if item.read_only || item.is_null || !resettable {
            return None;
        }
        let default = item.default.as_ref()?;
        // A nullable field whose default is `null` resets to the same place as
        // Inherit, so don't offer a redundant Reset.
        if item.nullable && default.is_null() {
            return None;
        }
        // Only when the current value actually differs from the default.
        if control_to_value(&item.control) == *default {
            return None;
        }
        Some(default.clone())
    }

    /// True when unsetting the field at `idx` makes it inherit a parent-scope
    /// value (e.g. `editor.line_wrap`) rather than simply clearing it.
    fn field_inherits(&self, idx: usize) -> bool {
        self.items
            .get(idx)
            .map(|item| {
                self.inheritable_fields
                    .contains(item.path.trim_start_matches('/'))
            })
            .unwrap_or(false)
    }

    /// The per-field action buttons to render at the right edge of the field at
    /// `idx`, left to right, with their labels. Empty when the field offers
    /// none (e.g. inherited/unset, at its default, or read-only). The
    /// `(Inherited)` badge is rendered separately.
    pub fn field_action_buttons(&self, idx: usize) -> Vec<(FieldAction, String)> {
        let Some(item) = self.items.get(idx) else {
            return Vec::new();
        };
        if item.read_only {
            return Vec::new();
        }
        let mut buttons = Vec::new();
        if self.reset_distinct_default(idx).is_some() {
            buttons.push((
                FieldAction::Reset,
                format!("[{}]", t!("settings.btn_reset")),
            ));
        }
        // "Set to null" is offered for any overriding nullable field (composite
        // ones too — they're click-only, see `field_focusable_count`). The label
        // is [Inherit] when null falls back to a parent value, else [Clear].
        if item.nullable && !item.is_null {
            let label = if self.field_inherits(idx) {
                t!("settings.btn_inherit")
            } else {
                t!("settings.btn_clear")
            };
            buttons.push((FieldAction::Inherit, format!("[{}]", label)));
        }
        buttons
    }

    /// Perform the action button at focusable index `i` for the field at `idx`.
    fn perform_field_action(&mut self, idx: usize, action: FieldAction) -> bool {
        match action {
            FieldAction::Reset => self.reset_field(idx),
            FieldAction::Inherit => self.inherit_field(idx),
        }
    }

    /// Activate the currently keyboard-focused field action button, if any.
    /// Returns true if a button was focused (so the key is consumed).
    pub fn activate_focused_field_button(&mut self) -> bool {
        let Some(i) = self.field_button_focus else {
            return false;
        };
        if self.focus_on_buttons {
            return false;
        }
        let idx = self.selected_item;
        if let Some(action) = self.field_action_buttons(idx).get(i).map(|(a, _)| *a) {
            self.perform_field_action(idx, action);
        }
        self.field_button_focus = None;
        self.update_focus_states();
        true
    }

    /// Convert dialog state back to JSON value (excludes the __key__ item)
    /// Auto-commit any draft text sitting in a TextList's trailing
    /// `[+] Add new` slot. Without this, saving a dialog while the user
    /// has typed (but not pressed Enter or ↓) into the new-item row
    /// silently drops that text — the diverging commit semantics
    /// between text fields ("typed value is just there") and list rows
    /// ("typing isn't enough — you must commit") was the F21 surprise.
    /// Run this from every save path so the saved value matches what
    /// the user sees on screen.
    pub fn commit_pending_list_drafts(&mut self) {
        for item in &mut self.items {
            if let SettingControl::TextList(state) = &mut item.control {
                if !state.new_item_text.is_empty() {
                    state.add_item();
                }
            }
        }
    }

    pub fn to_value(&self) -> Value {
        // For single-value dialogs (non-Object schemas like ObjectArray),
        // return the control's value directly instead of wrapping in an Object.
        if self.is_single_value {
            for item in &self.items {
                if item.path != "__key__" {
                    return control_to_value(&item.control);
                }
            }
        }

        let mut obj = serde_json::Map::new();

        for item in &self.items {
            // Skip the special key item - it's stored separately
            if item.path == "__key__" {
                continue;
            }

            let field_name = item.path.trim_start_matches('/');

            // Preserve inheritance: a nullable field whose value is inherited
            // (`is_null`) must NOT be written back as a concrete value, or it
            // stops inheriting from the global/default layer. `is_null` starts
            // true for inherited fields, is cleared the moment the user edits
            // the field (`mark_field_edited`), and is set again by the per-field
            // Inherit action — so it precisely tracks "did the user give this
            // field a value of its own?". Without this, opening a language entry
            // and toggling one field would freeze every *other* inherited field
            // (e.g. writing `line_wrap: false`), which then overrides the global
            // Toggle Line Wrap command forever (issue #2345).
            if item.nullable && item.is_null {
                continue;
            }

            let value = control_to_value(&item.control);
            obj.insert(field_name.to_string(), value);
        }

        Value::Object(obj)
    }

    /// Get currently selected item
    pub fn current_item(&self) -> Option<&SettingItem> {
        if self.focus_on_buttons {
            None
        } else {
            self.items.get(self.selected_item)
        }
    }

    /// Get currently selected item mutably
    pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
        if self.focus_on_buttons {
            None
        } else {
            self.items.get_mut(self.selected_item)
        }
    }

    /// Move focus to next editable item, navigating within composite controls first.
    ///
    /// For composite controls (Map, ObjectArray, TextList), Down first navigates
    /// through their internal entries and [+] Add new row before moving to the
    /// next dialog item. When at the last editable item, wraps to buttons.
    /// When on the last button, wraps back to the first editable item.
    /// Number of focusable per-field action buttons (`[Reset]`/`[Inherit]`/
    /// `[Clear]`) for the field at `idx`. Every field's buttons join the Tab
    /// order — for composite controls they come *after* the control's own
    /// internal sub-navigation (handled by `try_composite_focus_*`), so Tab is
    /// the sole keyboard path to these actions.
    fn field_focusable_count(&self, idx: usize) -> usize {
        self.field_action_buttons(idx).len()
    }

    /// Advance focus to the next *field* (control), skipping any per-field
    /// action buttons. Used by the form-style "commit and move on" flow
    /// (Enter/Tab/arrows while editing), where stopping on the field's own
    /// `[Reset]`/`[Inherit]` button would be surprising. Those buttons remain
    /// reachable via Tab in navigation mode.
    pub fn focus_next_field(&mut self) {
        if self.editing_text {
            return;
        }
        self.field_button_focus = None;
        if self.selected_item + 1 < self.items.len() {
            self.selected_item += 1;
            self.sub_focus = None;
            self.init_composite_focus(true);
        } else {
            self.focus_on_buttons = true;
            self.focused_button = 0;
        }
        self.update_focus_states();
        self.ensure_selected_visible(self.viewport_height);
    }

    /// Retreat focus to the previous *field* (control), skipping per-field
    /// action buttons. The arrow-key counterpart to [`focus_next_field`].
    pub fn focus_prev_field(&mut self) {
        if self.editing_text {
            return;
        }
        self.field_button_focus = None;
        if self.selected_item > self.first_editable_index {
            self.selected_item -= 1;
            self.sub_focus = None;
            self.init_composite_focus(false);
        } else {
            self.focus_on_buttons = true;
            self.focused_button = self.button_count().saturating_sub(1);
        }
        self.update_focus_states();
        self.ensure_selected_visible(self.viewport_height);
    }

    pub fn focus_next(&mut self) {
        if self.editing_text {
            return;
        }

        if self.focus_on_buttons {
            if self.focused_button + 1 < self.button_count() {
                self.focused_button += 1;
            } else {
                // Wrap to first editable item
                if self.first_editable_index < self.items.len() {
                    self.focus_on_buttons = false;
                    self.selected_item = self.first_editable_index;
                    self.sub_focus = None;
                    self.field_button_focus = None;
                    self.init_composite_focus(true);
                }
            }
        } else if let Some(i) = self.field_button_focus {
            // Advance through this field's action buttons, then to the next field.
            if i + 1 < self.field_focusable_count(self.selected_item) {
                self.field_button_focus = Some(i + 1);
            } else {
                self.field_button_focus = None;
                if self.selected_item + 1 < self.items.len() {
                    self.selected_item += 1;
                    self.sub_focus = None;
                    self.init_composite_focus(true);
                } else {
                    self.focus_on_buttons = true;
                    self.focused_button = 0;
                }
            }
        } else {
            // Try navigating within a composite control first
            let handled = self.try_composite_focus_next();
            if !handled {
                // Composite at its exit boundary (or not a composite). Stop on
                // this field's action buttons before advancing, if it has any.
                if self.field_focusable_count(self.selected_item) > 0 {
                    self.field_button_focus = Some(0);
                } else if self.selected_item + 1 < self.items.len() {
                    self.selected_item += 1;
                    self.sub_focus = None;
                    self.init_composite_focus(true);
                } else {
                    // Past last item, go to buttons
                    self.focus_on_buttons = true;
                    self.focused_button = 0;
                }
            }
        }

        self.update_focus_states();
        self.ensure_selected_visible(self.viewport_height);
    }

    /// Move focus to previous editable item, navigating within composite controls first.
    ///
    /// For composite controls, Up first navigates backwards through their internal
    /// entries before moving to the previous dialog item. When at the first editable
    /// item, wraps to buttons. When on the first button, wraps back to the last item.
    pub fn focus_prev(&mut self) {
        if self.editing_text {
            return;
        }

        if self.focus_on_buttons {
            if self.focused_button > 0 {
                self.focused_button -= 1;
            } else {
                // Wrap to last editable item
                if self.first_editable_index < self.items.len() {
                    self.focus_on_buttons = false;
                    self.selected_item = self.items.len().saturating_sub(1);
                    self.sub_focus = None;
                    self.init_composite_focus(false);
                    // Land on the field's last action button, if it has any.
                    self.field_button_focus = self
                        .field_focusable_count(self.selected_item)
                        .checked_sub(1);
                }
            }
        } else if let Some(i) = self.field_button_focus {
            // Step back through the action buttons, then to the control.
            self.field_button_focus = i.checked_sub(1);
        } else {
            // Try navigating within a composite control first
            let handled = self.try_composite_focus_prev();
            if !handled {
                // Composite is at its entry boundary (or not a composite) — go to previous item
                if self.selected_item > self.first_editable_index {
                    self.selected_item -= 1;
                    self.sub_focus = None;
                    self.init_composite_focus(false);
                    // Going backwards lands on the previous field's last
                    // element, which is its last action button when present.
                    self.field_button_focus = self
                        .field_focusable_count(self.selected_item)
                        .checked_sub(1);
                } else {
                    // Before first editable item, go to buttons
                    self.focus_on_buttons = true;
                    self.focused_button = self.button_count().saturating_sub(1);
                }
            }
        }

        self.update_focus_states();
        self.ensure_selected_visible(self.viewport_height);
    }

    /// Try to navigate forward within the current composite control.
    /// Returns true if the navigation was handled internally, false if at the exit boundary.
    fn try_composite_focus_next(&mut self) -> bool {
        let item = match self.items.get(self.selected_item) {
            Some(item) => item,
            None => return false,
        };
        match &item.control {
            SettingControl::Map(state) => {
                // Map returns bool: true = handled internally, false = at boundary
                let at_boundary = state.focused_entry.is_none(); // On add-new → exit
                if at_boundary {
                    return false;
                }
                if let Some(item) = self.items.get_mut(self.selected_item) {
                    if let SettingControl::Map(state) = &mut item.control {
                        return state.focus_next();
                    }
                }
                false
            }
            SettingControl::ObjectArray(state) => {
                // ObjectArray: None = on add-new → exit
                if state.focused_index.is_none() {
                    return false;
                }
                if let Some(item) = self.items.get_mut(self.selected_item) {
                    if let SettingControl::ObjectArray(state) = &mut item.control {
                        state.focus_next();
                        return true;
                    }
                }
                false
            }
            SettingControl::TextList(state) => {
                // TextList: None = on add-new → exit
                if state.focused_item.is_none() {
                    return false;
                }
                if let Some(item) = self.items.get_mut(self.selected_item) {
                    if let SettingControl::TextList(state) = &mut item.control {
                        state.focus_next();
                        return true;
                    }
                }
                false
            }
            _ => false,
        }
    }

    /// Try to navigate backward within the current composite control.
    /// Returns true if the navigation was handled internally, false if at the entry boundary.
    fn try_composite_focus_prev(&mut self) -> bool {
        let item = match self.items.get(self.selected_item) {
            Some(item) => item,
            None => return false,
        };
        match &item.control {
            SettingControl::Map(state) => {
                // Map: Some(0) = at first entry → exit
                let at_boundary = matches!(state.focused_entry, Some(0))
                    || (state.focused_entry.is_none() && state.entries.is_empty());
                if at_boundary {
                    return false;
                }
                if let Some(item) = self.items.get_mut(self.selected_item) {
                    if let SettingControl::Map(state) = &mut item.control {
                        return state.focus_prev();
                    }
                }
                false
            }
            SettingControl::ObjectArray(state) => {
                // ObjectArray: Some(0) = at first entry → exit
                if matches!(state.focused_index, Some(0))
                    || (state.focused_index.is_none() && state.bindings.is_empty())
                {
                    return false;
                }
                if let Some(item) = self.items.get_mut(self.selected_item) {
                    if let SettingControl::ObjectArray(state) = &mut item.control {
                        state.focus_prev();
                        return true;
                    }
                }
                false
            }
            SettingControl::TextList(state) => {
                // TextList: Some(0) = at first item → exit
                if matches!(state.focused_item, Some(0))
                    || (state.focused_item.is_none() && state.items.is_empty())
                {
                    return false;
                }
                if let Some(item) = self.items.get_mut(self.selected_item) {
                    if let SettingControl::TextList(state) = &mut item.control {
                        state.focus_prev();
                        return true;
                    }
                }
                false
            }
            _ => false,
        }
    }

    /// Initialize a composite control's focus when entering it.
    /// `from_above`: true = entering from the item above (start at first entry),
    ///               false = entering from below (start at add-new / last entry).
    fn init_composite_focus(&mut self, from_above: bool) {
        if let Some(item) = self.items.get_mut(self.selected_item) {
            match &mut item.control {
                SettingControl::Map(state) => {
                    state.init_focus(from_above);
                }
                SettingControl::ObjectArray(state) => {
                    if from_above {
                        state.focused_index = if state.bindings.is_empty() {
                            None
                        } else {
                            Some(0)
                        };
                    } else {
                        // Coming from below: start at add-new
                        state.focused_index = None;
                    }
                }
                SettingControl::TextList(state) => {
                    if from_above {
                        state.focused_item = if state.items.is_empty() {
                            None
                        } else {
                            Some(0)
                        };
                    } else {
                        // Coming from below: start at add-new
                        state.focused_item = None;
                    }
                }
                _ => {}
            }
        }
    }

    /// Toggle focus between items region and buttons region.
    /// Used by Tab key to provide region-level navigation.
    pub fn toggle_focus_region(&mut self) {
        self.toggle_focus_region_direction(true);
    }

    /// Toggle between items and buttons regions.
    /// When in buttons region, Tab cycles through buttons before returning to items.
    /// `forward` controls direction: true = Tab, false = Shift+Tab.
    pub fn toggle_focus_region_direction(&mut self, forward: bool) {
        if self.editing_text {
            return;
        }

        if self.focus_on_buttons {
            if forward {
                // Tab forward through buttons, then back to items
                if self.focused_button + 1 < self.button_count() {
                    self.focused_button += 1;
                } else {
                    // Past last button — return to items
                    if self.first_editable_index < self.items.len() {
                        self.focus_on_buttons = false;
                        if self.selected_item < self.first_editable_index {
                            self.selected_item = self.first_editable_index;
                        }
                    } else {
                        // All items read-only, wrap to first button
                        self.focused_button = 0;
                    }
                }
            } else {
                // Shift+Tab backward through buttons, then back to items
                if self.focused_button > 0 {
                    self.focused_button -= 1;
                } else {
                    // Before first button — return to items
                    if self.first_editable_index < self.items.len() {
                        self.focus_on_buttons = false;
                        if self.selected_item < self.first_editable_index {
                            self.selected_item = self.first_editable_index;
                        }
                    } else {
                        // All items read-only, wrap to last button
                        self.focused_button = self.button_count().saturating_sub(1);
                    }
                }
            }
        } else {
            // Move to buttons
            self.focus_on_buttons = true;
            self.focused_button = if forward {
                0
            } else {
                self.button_count().saturating_sub(1)
            };
        }

        self.update_focus_states();
        self.ensure_selected_visible(self.viewport_height);
    }

    /// Initialize composite control focus for the selected item (when dialog opens)
    fn init_object_array_focus(&mut self) {
        self.init_composite_focus(true);
    }

    /// Update focus states for all items
    pub fn update_focus_states(&mut self) {
        for (idx, item) in self.items.iter_mut().enumerate() {
            // When focus is on one of the field's action buttons, the control
            // itself is not the active element, so render it Normal — only the
            // button shows the focused highlight.
            let state = if !self.focus_on_buttons
                && idx == self.selected_item
                && self.field_button_focus.is_none()
            {
                FocusState::Focused
            } else {
                FocusState::Normal
            };

            match &mut item.control {
                SettingControl::Toggle(s) => s.focus = state,
                SettingControl::Number(s) => s.focus = state,
                SettingControl::Dropdown(s) => s.focus = state,
                SettingControl::Text(s) => s.focus = state,
                SettingControl::TextList(s) => s.focus = state,
                SettingControl::DualList(s) => s.focus = state,
                SettingControl::Map(s) => s.focus = state,
                SettingControl::ObjectArray(s) => s.focus = state,
                SettingControl::Json(s) => s.focus = state,
                SettingControl::Complex { .. } => {}
            }
        }
    }

    /// Height of a section header (label + blank line)
    const SECTION_HEADER_HEIGHT: usize = 2;

    /// Calculate total content height for all items (including separator and section headers)
    pub fn total_content_height(&self) -> usize {
        let items_height: usize = self
            .items
            .iter()
            .map(|item| {
                let section_h = if item.is_section_start {
                    Self::SECTION_HEADER_HEIGHT
                } else {
                    0
                };
                item.control.control_height() as usize + section_h
            })
            .sum();
        // Add 1 for separator if we have both read-only and editable items
        let separator_height =
            if self.first_editable_index > 0 && self.first_editable_index < self.items.len() {
                1
            } else {
                0
            };
        items_height + separator_height
    }

    /// Calculate the Y offset of the selected item (including separator and section headers)
    pub fn selected_item_offset(&self) -> usize {
        let items_offset: usize = self
            .items
            .iter()
            .take(self.selected_item)
            .map(|item| {
                let section_h = if item.is_section_start {
                    Self::SECTION_HEADER_HEIGHT
                } else {
                    0
                };
                item.control.control_height() as usize + section_h
            })
            .sum();
        // Add 1 for separator if selected item is after it
        let separator_offset = if self.first_editable_index > 0
            && self.first_editable_index < self.items.len()
            && self.selected_item >= self.first_editable_index
        {
            1
        } else {
            0
        };
        // Add section header height if the selected item itself starts a section
        let own_section_h = self
            .items
            .get(self.selected_item)
            .map(|item| {
                if item.is_section_start {
                    Self::SECTION_HEADER_HEIGHT
                } else {
                    0
                }
            })
            .unwrap_or(0);
        items_offset + separator_offset + own_section_h
    }

    /// Calculate the height of the selected item
    pub fn selected_item_height(&self) -> usize {
        self.items
            .get(self.selected_item)
            .map(|item| item.control.control_height() as usize)
            .unwrap_or(1)
    }

    /// Ensure the selected item is visible within the viewport
    pub fn ensure_selected_visible(&mut self, viewport_height: usize) {
        if self.focus_on_buttons {
            // Scroll to bottom when buttons are focused
            let total = self.total_content_height();
            if total > viewport_height {
                self.scroll_offset = total.saturating_sub(viewport_height);
            }
            return;
        }

        let item_start = self.selected_item_offset();
        let item_end = item_start + self.selected_item_height();

        // If item starts before viewport, scroll up
        if item_start < self.scroll_offset {
            self.scroll_offset = item_start;
        }
        // If item ends after viewport, scroll down
        else if item_end > self.scroll_offset + viewport_height {
            self.scroll_offset = item_end.saturating_sub(viewport_height);
        }
    }

    /// Ensure the cursor within a JSON editor is visible
    ///
    /// When editing a multiline JSON control, this adjusts scroll_offset
    /// to keep the cursor row visible within the viewport.
    pub fn ensure_cursor_visible(&mut self) {
        if !self.editing_text || self.focus_on_buttons {
            return;
        }

        // Get cursor row from current item (if it's a JSON editor)
        let cursor_row = if let Some(item) = self.items.get(self.selected_item) {
            if let SettingControl::Json(state) = &item.control {
                state.cursor_pos().0
            } else {
                return; // Not a JSON editor
            }
        } else {
            return;
        };

        // Calculate absolute position of cursor row in content:
        // item_offset + 1 (for label row) + cursor_row
        let item_offset = self.selected_item_offset();
        let cursor_content_row = item_offset + 1 + cursor_row;

        let viewport_height = self.viewport_height;

        // If cursor is above viewport, scroll up
        if cursor_content_row < self.scroll_offset {
            self.scroll_offset = cursor_content_row;
        }
        // If cursor is below viewport, scroll down
        else if cursor_content_row >= self.scroll_offset + viewport_height {
            self.scroll_offset = cursor_content_row.saturating_sub(viewport_height) + 1;
        }
    }

    /// Scroll up by one line
    pub fn scroll_up(&mut self) {
        self.scroll_offset = self.scroll_offset.saturating_sub(1);
    }

    /// Scroll down by one line
    pub fn scroll_down(&mut self, viewport_height: usize) {
        let max_scroll = self.total_content_height().saturating_sub(viewport_height);
        if self.scroll_offset < max_scroll {
            self.scroll_offset += 1;
        }
    }

    /// Scroll to a position based on ratio (0.0 = top, 1.0 = bottom)
    ///
    /// Used for scrollbar drag operations.
    pub fn scroll_to_ratio(&mut self, ratio: f32) {
        let max_scroll = self
            .total_content_height()
            .saturating_sub(self.viewport_height);
        let new_offset = (ratio * max_scroll as f32).round() as usize;
        self.scroll_offset = new_offset.min(max_scroll);
    }

    /// Start text editing mode for the current control
    pub fn start_editing(&mut self) {
        // Snapshot the field *before* mutating it, so Esc can revert an
        // abandoned edit to exactly what it was. Only the editable text
        // controls below enter edit mode, so only those are worth snapshotting.
        if !self.focus_on_buttons {
            if let Some(item) = self.items.get(self.selected_item) {
                if item.read_only {
                    return;
                }
                if matches!(
                    item.control,
                    SettingControl::Text(_)
                        | SettingControl::TextList(_)
                        | SettingControl::Number(_)
                        | SettingControl::Json(_)
                ) {
                    self.edit_snapshot = Some(FieldEditSnapshot {
                        item_index: self.selected_item,
                        control: item.control.clone(),
                        is_null: item.is_null,
                        user_edited: self.user_edited,
                    });
                }
            }
        }
        if let Some(item) = self.current_item_mut() {
            // Don't allow editing read-only fields
            if item.read_only {
                return;
            }
            match &mut item.control {
                SettingControl::Text(state) => {
                    state.editor.move_end();
                    state.editing = true;
                    self.editing_text = true;
                }
                SettingControl::TextList(state) => {
                    // If focused on a committed item, leave focus there
                    // and just flip into edit mode. Otherwise (focus on
                    // the trailing `[+] Add new` slot), explicitly
                    // activate input mode so the row morphs from
                    // `[+] Add new` into the bracketed input box.
                    if state.focused_item.is_none() {
                        state.activate_pending();
                    } else {
                        // Editing a committed item: flip on the caret.
                        state.editing = true;
                    }
                    self.editing_text = true;
                }
                SettingControl::Number(state) => {
                    state.start_editing();
                    self.editing_text = true;
                }
                SettingControl::Json(state) => {
                    // Wipe the `null` placeholder so typing replaces it
                    // instead of concatenating onto the literal text.
                    state.clear_placeholder_for_edit();
                    self.editing_text = true;
                }
                _ => {}
            }
        }
    }

    /// Commit text editing mode. This is the *accept* path — Enter, Tab, and
    /// clicking away (blur) all land here, following the platform convention
    /// that those gestures keep the typed value. Esc takes `revert_editing`
    /// instead.
    pub fn stop_editing(&mut self) {
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                // A number's typed digits live in a separate edit buffer that
                // only flushes into `value` on confirm; without this every
                // tab-size / page-width edit reverted to the old value on
                // commit. Confirm so the typed value sticks.
                SettingControl::Number(state) => state.confirm_editing(),
                SettingControl::Text(state) => state.editing = false,
                // On the trailing `[+] Add new` slot the pending draft has
                // already been flushed to the list by the Enter/Tab handler;
                // collapse the slot back to `[+] Add new`.
                SettingControl::TextList(state) => {
                    if state.focused_item.is_none() {
                        state.cancel_pending();
                    }
                    state.editing = false;
                }
                // If the user opened a JSON field but didn't type anything (or
                // deleted everything), put the `null` sentinel back so the
                // value still round-trips as JSON.
                SettingControl::Json(state) => state.restore_unset_if_empty(),
                _ => {}
            }
        }
        // The edit was accepted — there is nothing left to revert.
        self.edit_snapshot = None;
        self.editing_text = false;
    }

    /// Cancel text editing mode, discarding the in-progress edit and restoring
    /// the field to its pre-edit state. This is the Esc path — matching the
    /// Windows/macOS/web convention where Esc reverts an edit while Enter and
    /// Tab commit it. If no snapshot was captured (e.g. a control that mutates
    /// nothing until commit), this still exits edit mode cleanly.
    pub fn revert_editing(&mut self) {
        if let Some(snap) = self.edit_snapshot.take() {
            if let Some(item) = self.items.get_mut(snap.item_index) {
                item.control = snap.control;
                item.is_null = snap.is_null;
            }
            // Restore the dialog's dirty flag to what it was before this edit,
            // so reverting the *only* change also drops the "unsaved" state
            // while preserving edits made to other fields earlier.
            self.user_edited = snap.user_edited;
        }
        self.editing_text = false;
    }

    /// Handle character input
    pub fn insert_char(&mut self, c: char) {
        if !self.editing_text {
            return;
        }
        self.mark_field_edited();
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Text(state) => {
                    state.insert(c);
                }
                SettingControl::TextList(state) => {
                    state.insert(c);
                }
                SettingControl::Number(state) => {
                    state.insert_char(c);
                }
                SettingControl::Json(state) => {
                    state.insert(c);
                }
                _ => {}
            }
        }
    }

    pub fn insert_str(&mut self, s: &str) {
        if !self.editing_text {
            return;
        }
        self.mark_field_edited();
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Text(state) => {
                    state.insert_str(s);
                }
                SettingControl::TextList(state) => {
                    state.insert_str(s);
                }
                SettingControl::Number(state) => {
                    for c in s.chars() {
                        state.insert_char(c);
                    }
                }
                SettingControl::Json(state) => {
                    state.insert_str(s);
                }
                _ => {}
            }
        }
    }

    /// Handle backspace
    pub fn backspace(&mut self) {
        if !self.editing_text {
            return;
        }
        self.mark_field_edited();
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Text(state) => {
                    state.backspace();
                }
                SettingControl::TextList(state) => {
                    state.backspace();
                }
                SettingControl::Number(state) => {
                    state.backspace();
                }
                SettingControl::Json(state) => {
                    state.backspace();
                }
                _ => {}
            }
        }
    }

    /// Handle cursor left
    pub fn cursor_left(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Text(state) => {
                    state.move_left();
                }
                SettingControl::TextList(state) => {
                    state.move_left();
                }
                SettingControl::Json(state) => {
                    state.move_left();
                }
                _ => {}
            }
        }
    }

    /// Handle cursor left with selection (Shift+Left)
    pub fn cursor_left_selecting(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Json(state) => state.editor.move_left_selecting(),
                SettingControl::Text(state) => state.move_left_selecting(),
                // Controls without a selection model keep the historical
                // "Shift is ignored" behavior: a plain move.
                SettingControl::TextList(state) => state.move_left(),
                _ => {}
            }
        }
    }

    /// Handle cursor right
    pub fn cursor_right(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Text(state) => {
                    state.move_right();
                }
                SettingControl::TextList(state) => {
                    state.move_right();
                }
                SettingControl::Json(state) => {
                    state.move_right();
                }
                _ => {}
            }
        }
    }

    /// Handle cursor right with selection (Shift+Right)
    pub fn cursor_right_selecting(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Json(state) => state.editor.move_right_selecting(),
                SettingControl::Text(state) => state.move_right_selecting(),
                // Controls without a selection model keep the historical
                // "Shift is ignored" behavior: a plain move.
                SettingControl::TextList(state) => state.move_right(),
                _ => {}
            }
        }
    }

    /// Handle cursor up (for multiline controls)
    pub fn cursor_up(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Json(state) = &mut item.control {
                state.move_up();
            }
        }
        self.ensure_cursor_visible();
    }

    /// Handle cursor up with selection (Shift+Up)
    pub fn cursor_up_selecting(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Json(state) = &mut item.control {
                state.editor.move_up_selecting();
            }
        }
        self.ensure_cursor_visible();
    }

    /// Handle cursor down (for multiline controls)
    pub fn cursor_down(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Json(state) = &mut item.control {
                state.move_down();
            }
        }
        self.ensure_cursor_visible();
    }

    /// Handle cursor down with selection (Shift+Down)
    pub fn cursor_down_selecting(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Json(state) = &mut item.control {
                state.editor.move_down_selecting();
            }
        }
        self.ensure_cursor_visible();
    }

    /// Insert newline in JSON editor
    pub fn insert_newline(&mut self) {
        self.mark_field_edited();
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Json(state) = &mut item.control {
                state.insert('\n');
            }
        }
    }

    /// Revert JSON changes to original and stop editing
    pub fn revert_json_and_stop(&mut self) {
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Json(state) = &mut item.control {
                state.revert();
            }
        }
        self.editing_text = false;
    }

    /// Check if current control is a JSON editor
    pub fn is_editing_json(&self) -> bool {
        if !self.editing_text {
            return false;
        }
        self.current_item()
            .map(|item| matches!(&item.control, SettingControl::Json(_)))
            .unwrap_or(false)
    }

    /// Toggle boolean value
    pub fn toggle_bool(&mut self) {
        // Don't allow toggling read-only / non-toggle fields, and don't flag
        // the dialog dirty when nothing actually changes.
        let editable = self
            .current_item()
            .map(|i| !i.read_only && matches!(i.control, SettingControl::Toggle(_)))
            .unwrap_or(false);
        if !editable {
            return;
        }
        self.mark_field_edited();
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Toggle(state) = &mut item.control {
                state.toggle();
            }
        }
    }

    /// Toggle dropdown open state
    pub fn toggle_dropdown(&mut self) {
        if let Some(item) = self.current_item_mut() {
            // Don't allow editing read-only fields
            if item.read_only {
                return;
            }
            if let SettingControl::Dropdown(state) = &mut item.control {
                state.open = !state.open;
            }
        }
    }

    /// Move dropdown selection up
    pub fn dropdown_prev(&mut self) {
        self.mark_field_edited();
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Dropdown(state) = &mut item.control {
                if state.open {
                    state.select_prev();
                }
            }
        }
    }

    /// Move dropdown selection down
    pub fn dropdown_next(&mut self) {
        self.mark_field_edited();
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Dropdown(state) = &mut item.control {
                if state.open {
                    state.select_next();
                }
            }
        }
    }

    /// Confirm dropdown selection
    pub fn dropdown_confirm(&mut self) {
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::Dropdown(state) = &mut item.control {
                state.open = false;
            }
        }
    }

    /// Delete the currently focused item from a TextList control
    pub fn delete_list_item(&mut self) {
        self.mark_field_edited();
        if let Some(item) = self.current_item_mut() {
            if let SettingControl::TextList(state) = &mut item.control {
                // Remove the currently focused item if any
                if let Some(idx) = state.focused_item {
                    state.remove_item(idx);
                }
            }
        }
    }

    /// Delete character at cursor (forward delete)
    pub fn delete(&mut self) {
        if !self.editing_text {
            return;
        }
        self.mark_field_edited();
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Text(state) => {
                    state.delete();
                }
                SettingControl::TextList(state) => {
                    state.delete();
                }
                SettingControl::Json(state) => {
                    state.delete();
                }
                _ => {}
            }
        }
    }

    /// Move cursor to beginning of line
    pub fn cursor_home(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Text(state) => {
                    state.move_home();
                }
                SettingControl::TextList(state) => {
                    state.move_home();
                }
                SettingControl::Json(state) => {
                    state.move_home();
                }
                _ => {}
            }
        }
    }

    /// Move cursor to end of line
    pub fn cursor_end(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Text(state) => {
                    state.move_end();
                }
                SettingControl::TextList(state) => {
                    state.move_end();
                }
                SettingControl::Json(state) => {
                    state.move_end();
                }
                _ => {}
            }
        }
    }

    /// Select all text in current control
    pub fn select_all(&mut self) {
        if !self.editing_text {
            return;
        }
        if let Some(item) = self.current_item_mut() {
            match &mut item.control {
                SettingControl::Json(state) => state.select_all(),
                SettingControl::Text(state) => state.select_all(),
                _ => {}
            }
        }
    }

    /// Get selected text from the current text-editing control
    pub fn selected_text(&self) -> Option<String> {
        if !self.editing_text {
            return None;
        }
        if let Some(item) = self.current_item() {
            match &item.control {
                SettingControl::Json(state) => return state.selected_text(),
                SettingControl::Text(state) => return state.selected_text(),
                _ => {}
            }
        }
        None
    }

    /// Check if any field is currently in edit mode
    pub fn is_editing(&self) -> bool {
        self.editing_text
            || self
                .current_item()
                .map(|item| {
                    matches!(
                        &item.control,
                        SettingControl::Dropdown(s) if s.open
                    )
                })
                .unwrap_or(false)
    }
}

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

    fn create_test_schema() -> SettingSchema {
        SettingSchema {
            path: "/test".to_string(),
            name: "Test".to_string(),
            description: Some("Test schema".to_string()),
            setting_type: SettingType::Object {
                properties: vec![
                    SettingSchema {
                        path: "/enabled".to_string(),
                        name: "Enabled".to_string(),
                        description: Some("Enable this".to_string()),
                        setting_type: SettingType::Boolean,
                        default: Some(serde_json::json!(true)),
                        read_only: false,
                        section: None,
                        order: None,
                        nullable: false,
                        enum_from: None,
                        dual_list_sibling: None,
                        dynamically_extendable_status_bar_elements: false,
                    },
                    SettingSchema {
                        path: "/command".to_string(),
                        name: "Command".to_string(),
                        description: Some("Command to run".to_string()),
                        setting_type: SettingType::String,
                        default: Some(serde_json::json!("")),
                        read_only: false,
                        section: None,
                        order: None,
                        nullable: false,
                        enum_from: None,
                        dual_list_sibling: None,
                        dynamically_extendable_status_bar_elements: false,
                    },
                ],
            },
            default: None,
            read_only: false,
            section: None,
            order: None,
            nullable: false,
            enum_from: None,
            dual_list_sibling: None,
            dynamically_extendable_status_bar_elements: false,
        }
    }

    #[test]
    fn from_schema_creates_key_item_first() {
        let schema = create_test_schema();
        let dialog = EntryDialogState::from_schema(
            "test".to_string(),
            &serde_json::json!({}),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );

        assert!(!dialog.items.is_empty());
        assert_eq!(dialog.items[0].path, "__key__");
        assert_eq!(dialog.items[0].name, "Key");
    }

    #[test]
    fn from_schema_creates_items_from_properties() {
        let schema = create_test_schema();
        let dialog = EntryDialogState::from_schema(
            "test".to_string(),
            &serde_json::json!({"enabled": true, "command": "test-cmd"}),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );

        // Key + 2 properties = 3 items
        assert_eq!(dialog.items.len(), 3);
        assert_eq!(dialog.items[1].name, "Enabled");
        assert_eq!(dialog.items[2].name, "Command");
    }

    #[test]
    fn get_key_returns_key_value() {
        let schema = create_test_schema();
        let dialog = EntryDialogState::from_schema(
            "mykey".to_string(),
            &serde_json::json!({}),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );

        assert_eq!(dialog.get_key(), "mykey");
    }

    #[test]
    fn to_value_excludes_key() {
        let schema = create_test_schema();
        let dialog = EntryDialogState::from_schema(
            "test".to_string(),
            &serde_json::json!({"enabled": true, "command": "cmd"}),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );

        let value = dialog.to_value();
        assert!(value.get("__key__").is_none());
        assert!(value.get("enabled").is_some());
    }

    #[test]
    fn focus_navigation_works() {
        let schema = create_test_schema();
        let mut dialog = EntryDialogState::from_schema(
            "test".to_string(),
            &serde_json::json!({}),
            &schema,
            "/test",
            false, // existing entry - Key is read-only
            false, // allow delete
            &HashMap::new(),
        );

        // With is_new=false, Key is read-only and sorted first
        // Items: [Key (read-only), Enabled, Command]
        // Focus starts at first editable item (index 1)
        assert_eq!(dialog.first_editable_index, 1);
        assert_eq!(dialog.selected_item, 1); // First editable (Enabled)
        assert!(!dialog.focus_on_buttons);

        dialog.focus_next();
        assert_eq!(dialog.selected_item, 2); // Command

        dialog.focus_next();
        assert!(dialog.focus_on_buttons); // No more editable items
        assert_eq!(dialog.focused_button, 0);

        // Going back should skip read-only Key
        dialog.focus_prev();
        assert!(!dialog.focus_on_buttons);
        assert_eq!(dialog.selected_item, 2); // Last editable (Command)

        dialog.focus_prev();
        assert_eq!(dialog.selected_item, 1); // First editable (Enabled)

        dialog.focus_prev();
        assert!(dialog.focus_on_buttons); // Wraps to buttons, not to read-only Key
    }

    /// A nullable field whose built-in default is itself non-null *and* is
    /// currently overridden to a third value offers both `[Reset]` and
    /// `[Inherit]`. Tab must step control → Reset → Inherit → (footer), and
    /// Shift+Tab must reverse exactly: (footer) → Inherit → Reset → control.
    #[test]
    fn focus_cycles_through_both_field_action_buttons() {
        let schema = SettingSchema {
            path: "/test".to_string(),
            name: "Test".to_string(),
            description: None,
            setting_type: SettingType::Object {
                properties: vec![SettingSchema {
                    path: "/wrap".to_string(),
                    name: "Wrap".to_string(),
                    description: None,
                    setting_type: SettingType::Boolean,
                    // Non-null built-in default, so Reset (→true) differs from
                    // Inherit (→null).
                    default: Some(serde_json::json!(true)),
                    read_only: false,
                    section: None,
                    order: None,
                    nullable: true,
                    enum_from: None,
                    dual_list_sibling: None,
                    dynamically_extendable_status_bar_elements: false,
                }],
            },
            default: None,
            read_only: false,
            section: None,
            order: None,
            nullable: false,
            enum_from: None,
            dual_list_sibling: None,
            dynamically_extendable_status_bar_elements: false,
        };
        // Overridden to `false` — distinct from both the default (true) and
        // inherit (null).
        let mut dialog = EntryDialogState::from_schema(
            "k".to_string(),
            &serde_json::json!({ "wrap": false }),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );

        // Field is index 1 (after read-only Key) and offers both buttons.
        assert_eq!(dialog.selected_item, 1);
        let buttons = dialog.field_action_buttons(1);
        assert_eq!(
            buttons.iter().map(|(a, _)| *a).collect::<Vec<_>>(),
            vec![FieldAction::Reset, FieldAction::Inherit]
        );
        assert_eq!(dialog.field_button_focus, None);

        // Forward: control → Reset → Inherit → footer.
        dialog.focus_next();
        assert_eq!(dialog.field_button_focus, Some(0)); // Reset
        dialog.focus_next();
        assert_eq!(dialog.field_button_focus, Some(1)); // Inherit
        dialog.focus_next();
        assert!(dialog.focus_on_buttons);

        // Backward: footer → Inherit → Reset → control.
        dialog.focus_prev();
        assert!(!dialog.focus_on_buttons);
        assert_eq!(dialog.field_button_focus, Some(1)); // Inherit
        dialog.focus_prev();
        assert_eq!(dialog.field_button_focus, Some(0)); // Reset
        dialog.focus_prev();
        assert_eq!(dialog.field_button_focus, None); // back on the control
        dialog.focus_prev();
        assert!(dialog.focus_on_buttons); // before first field → footer
    }

    /// A JSON/object field (like a language `formatter`) is not a "simple"
    /// control, but its per-field action buttons must still be reachable by Tab
    /// — that's the only keyboard path now that Ctrl+R is gone.
    #[test]
    fn focus_reaches_action_buttons_on_json_field() {
        let schema = SettingSchema {
            path: "/test".to_string(),
            name: "Test".to_string(),
            description: None,
            setting_type: SettingType::Object {
                properties: vec![SettingSchema {
                    path: "/formatter".to_string(),
                    name: "Formatter".to_string(),
                    description: None,
                    // Object => rendered as a JSON control.
                    setting_type: SettingType::Object { properties: vec![] },
                    default: Some(serde_json::json!({ "command": "clang-format" })),
                    read_only: false,
                    section: None,
                    order: None,
                    nullable: true,
                    enum_from: None,
                    dual_list_sibling: None,
                    dynamically_extendable_status_bar_elements: false,
                }],
            },
            default: None,
            read_only: false,
            section: None,
            order: None,
            nullable: false,
            enum_from: None,
            dual_list_sibling: None,
            dynamically_extendable_status_bar_elements: false,
        };
        // Overridden to a different command, so it differs from the default.
        let mut dialog = EntryDialogState::from_schema(
            "c".to_string(),
            &serde_json::json!({ "formatter": { "command": "my-fmt" } }),
            &schema,
            "/languages",
            false,
            false,
            &HashMap::new(),
        );

        // The JSON field offers buttons (at least [Reset]); Tab steps onto them.
        assert_eq!(dialog.selected_item, 1);
        assert!(
            !dialog.field_action_buttons(1).is_empty(),
            "overridden JSON field should offer action buttons"
        );
        assert_eq!(dialog.field_button_focus, None);
        dialog.focus_next();
        assert_eq!(
            dialog.field_button_focus,
            Some(0),
            "Tab should land on the JSON field's first action button"
        );
    }

    #[test]
    fn entry_path_joins_map_path_and_entry_key() {
        let schema = create_test_schema();

        // Existing entry: full path is map_path + "/" + entry_key
        let existing = EntryDialogState::from_schema(
            "rust".to_string(),
            &serde_json::json!({}),
            &schema,
            "/lsp",
            false,
            false,
            &HashMap::new(),
        );
        assert_eq!(existing.entry_path(), "/lsp/rust");

        // New entry with no key typed yet falls back to the parent map path.
        // Nested dialogs keyed off this are outside the scope of this test.
        let new_entry = EntryDialogState::from_schema(
            String::new(),
            &serde_json::json!({}),
            &schema,
            "/lsp",
            true,
            false,
            &HashMap::new(),
        );
        assert_eq!(new_entry.entry_path(), "/lsp");
    }

    #[test]
    fn entry_path_tracks_live_key_edits_for_new_entries() {
        let schema = create_test_schema();
        let mut dialog = EntryDialogState::from_schema(
            String::new(),
            &serde_json::json!({}),
            &schema,
            "/universal_lsp",
            true,
            false,
            &HashMap::new(),
        );

        // User types a key into the editable key field.
        for item in dialog.items.iter_mut() {
            if item.path == "__key__" {
                if let SettingControl::Text(state) = &mut item.control {
                    state.set_value("myserver");
                }
            }
        }

        assert_eq!(dialog.entry_path(), "/universal_lsp/myserver");
    }

    #[test]
    fn button_count_differs_for_new_vs_existing() {
        let schema = create_test_schema();

        let new_dialog = EntryDialogState::from_schema(
            "test".to_string(),
            &serde_json::json!({}),
            &schema,
            "/test",
            true,
            false,
            &HashMap::new(),
        );
        assert_eq!(new_dialog.button_count(), 2); // Save, Cancel

        let existing_dialog = EntryDialogState::from_schema(
            "test".to_string(),
            &serde_json::json!({}),
            &schema,
            "/test",
            false,
            false, // allow delete
            &HashMap::new(),
        );
        assert_eq!(existing_dialog.button_count(), 3); // Save, Delete, Cancel

        // no_delete hides the Delete button even for existing entries
        let no_delete_dialog = EntryDialogState::from_schema(
            "test".to_string(),
            &serde_json::json!({}),
            &schema,
            "/test",
            false,
            true, // no delete (auto-managed entries like plugins)
            &HashMap::new(),
        );
        assert_eq!(no_delete_dialog.button_count(), 2); // Save, Cancel (no Delete)
    }

    // ---- Esc-reverts / Enter-commits field editing --------------------------

    /// Build a single-property schema whose one field has the given type, so a
    /// dialog can be driven straight to that control.
    fn prop_schema(path: &str, name: &str, ty: SettingType) -> SettingSchema {
        SettingSchema {
            path: "/test".to_string(),
            name: "Test".to_string(),
            description: None,
            setting_type: SettingType::Object {
                properties: vec![SettingSchema {
                    path: path.to_string(),
                    name: name.to_string(),
                    description: None,
                    setting_type: ty,
                    default: None,
                    read_only: false,
                    section: None,
                    order: None,
                    nullable: false,
                    enum_from: None,
                    dual_list_sibling: None,
                    dynamically_extendable_status_bar_elements: false,
                }],
            },
            default: None,
            read_only: false,
            section: None,
            order: None,
            nullable: false,
            enum_from: None,
            dual_list_sibling: None,
            dynamically_extendable_status_bar_elements: false,
        }
    }

    /// Focus the field at `path` (leaving button focus), returning its index.
    fn select_field(dialog: &mut EntryDialogState, path: &str) -> usize {
        let idx = dialog
            .items
            .iter()
            .position(|it| it.path == path)
            .expect("field should exist");
        dialog.selected_item = idx;
        dialog.focus_on_buttons = false;
        idx
    }

    /// Esc reverts a Text field to its original string; the edit is discarded.
    #[test]
    fn esc_reverts_text_field() {
        let schema = prop_schema("/grammar", "Grammar", SettingType::String);
        let mut dialog = EntryDialogState::from_schema(
            "k".to_string(),
            &serde_json::json!({ "grammar": "typescript" }),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );
        let idx = select_field(&mut dialog, "/grammar");

        dialog.start_editing();
        dialog.insert_char('X');
        dialog.insert_char('Y');
        assert_eq!(
            control_to_value(&dialog.items[idx].control),
            serde_json::json!("typescriptXY"),
            "sanity: the edit is applied live before reverting"
        );

        dialog.revert_editing();
        assert!(!dialog.editing_text, "revert exits edit mode");
        assert_eq!(
            control_to_value(&dialog.items[idx].control),
            serde_json::json!("typescript"),
            "Esc must restore the original text"
        );
    }

    /// Esc reverts a Number field to its original value (typed digits dropped).
    #[test]
    fn esc_reverts_number_field() {
        let schema = prop_schema(
            "/tab_size",
            "Tab Size",
            SettingType::Integer {
                minimum: None,
                maximum: None,
            },
        );
        let mut dialog = EntryDialogState::from_schema(
            "k".to_string(),
            &serde_json::json!({ "tab_size": 4 }),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );
        let idx = select_field(&mut dialog, "/tab_size");

        dialog.start_editing();
        dialog.insert_char('9'); // replaces the selected "4" in the edit buffer
        dialog.revert_editing();

        assert_eq!(
            control_to_value(&dialog.items[idx].control),
            serde_json::json!(4),
            "Esc must restore the original number"
        );
    }

    /// Esc reverts an in-place edit of an existing TextList item — the case the
    /// old accept-on-Esc path silently kept.
    #[test]
    fn esc_reverts_textlist_item_edit() {
        let schema = prop_schema("/extensions", "Extensions", SettingType::StringArray);
        let mut dialog = EntryDialogState::from_schema(
            "k".to_string(),
            &serde_json::json!({ "extensions": ["ts", "tsx"] }),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );
        let idx = select_field(&mut dialog, "/extensions");

        // Put the control into "editing the first existing item" state.
        if let SettingControl::TextList(state) = &mut dialog.items[idx].control {
            state.focused_item = Some(0);
            state.cursor = state.items[0].len();
        }
        dialog.start_editing();
        dialog.insert_char('Z'); // "ts" -> "tsZ"
        assert_eq!(
            control_to_value(&dialog.items[idx].control),
            serde_json::json!(["tsZ", "tsx"]),
            "sanity: the item is mutated live before reverting"
        );

        dialog.revert_editing();
        assert_eq!(
            control_to_value(&dialog.items[idx].control),
            serde_json::json!(["ts", "tsx"]),
            "Esc must restore the original list items"
        );
    }

    /// Esc reverts a JSON/object field (a language `formatter`) to its original.
    #[test]
    fn esc_reverts_json_field() {
        let schema = prop_schema("/formatter", "Formatter", SettingType::Complex);
        let mut dialog = EntryDialogState::from_schema(
            "k".to_string(),
            &serde_json::json!({ "formatter": { "command": "prettier" } }),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );
        let idx = select_field(&mut dialog, "/formatter");

        dialog.start_editing();
        dialog.insert_char('X'); // corrupt the JSON text in the editor
        dialog.revert_editing();

        assert_eq!(
            control_to_value(&dialog.items[idx].control),
            serde_json::json!({ "command": "prettier" }),
            "Esc must restore the original JSON value"
        );
    }

    /// The commit path is unchanged: Enter/Tab (via `stop_editing`) keep the
    /// typed value, so the two keys are genuinely distinct.
    #[test]
    fn stop_editing_commits_text_field() {
        let schema = prop_schema("/grammar", "Grammar", SettingType::String);
        let mut dialog = EntryDialogState::from_schema(
            "k".to_string(),
            &serde_json::json!({ "grammar": "typescript" }),
            &schema,
            "/test",
            false,
            false,
            &HashMap::new(),
        );
        let idx = select_field(&mut dialog, "/grammar");

        dialog.start_editing();
        dialog.insert_char('X');
        dialog.stop_editing();

        assert_eq!(
            control_to_value(&dialog.items[idx].control),
            serde_json::json!("typescriptX"),
            "Enter/Tab must keep the typed value"
        );
    }
}
