// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Interactive plugin configuration editing.
//!
//! Keep this module focused on TTY and `dialoguer` orchestration. New testable plugin config
//! behavior should live in `plugins/config_io.rs` or `plugins/editor_model.rs`, with focused unit
//! tests, so Codecov does not depend on exercising interactive prompt loops.

use std::io::IsTerminal;
use std::path::Path;

use console::{Key, Term, style};
use dialoguer::theme::ColorfulTheme;
use dialoguer::{Input, Select};
use nemo_flow::config_editor::{EditorConfig, EditorFieldKind, EditorFieldSpec};
use nemo_flow::observability::plugin_component::ObservabilityConfig;
use nemo_flow_adaptive::AdaptiveConfig;
use serde_json::{Value, json};

use crate::config::PluginsEditCommand;
use crate::error::CliError;

mod config_io;
mod editor_model;

use self::config_io::*;
use self::editor_model::*;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MenuShortcut {
    Preview,
    Save,
    Help,
    Reset,
    Clear,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MenuResponse {
    Selected(usize),
    Shortcut(MenuShortcut, usize),
    Cancel,
}

#[derive(Debug)]
struct MenuItem {
    label: String,
}

impl MenuItem {
    fn new(label: impl Into<String>) -> Self {
        Self {
            label: label.into(),
        }
    }
}

fn status_label(enabled: bool) -> String {
    if enabled {
        style("on").green().to_string()
    } else {
        style("off").red().to_string()
    }
}

fn shortcut_label(label: impl AsRef<str>, shortcut: &str) -> String {
    format!(
        "{} {}",
        label.as_ref(),
        style(format!("[{shortcut}]")).black().bright()
    )
}

fn configured_label(configured: bool, label: impl AsRef<str>) -> String {
    if configured {
        format!("{} {}", style("✓").green(), label.as_ref())
    } else {
        format!("  {}", label.as_ref())
    }
}

fn print_save_success(path: &Path) {
    println!("  {} Saved {}", style("✔").green(), path.display());
}

pub(crate) fn edit(command: PluginsEditCommand) -> Result<(), CliError> {
    ensure_tty()?;
    let scope = target_scope(&command)?;
    let path = target_path(scope)?;
    let mut config = read_plugin_config(&path)?;
    ensure_observability_component(&mut config)?;
    ensure_adaptive_component(&mut config)?;
    let mut observability = component_observability_config(&config)?;
    let mut adaptive = component_adaptive_config(&config)?;

    let theme = ColorfulTheme::default();
    crate::banner::print_intro();
    println!("  Editing plugin config at {}", path.display());
    println!("  Tip: ↑/↓ or j/k to move, SPACE/ENTER to select, p to preview, s to save.");
    println!();
    let mut selected_index = 0;
    loop {
        let summary = observability_summary(&config, &observability);
        let adaptive_summary = adaptive_summary(&config, &adaptive);
        let observability_fields = ObservabilityConfig::editor_schema().fields;
        let adaptive_fields = AdaptiveConfig::editor_schema().fields;
        let mut items = vec![MenuItem::new(format!(
            "Toggle Observability component [{}]",
            status_label(component_enabled(&config))
        ))];
        items.extend(observability_fields.iter().map(|section| {
            MenuItem::new(configured_label(
                section_configured(&observability, *section),
                format!("Edit {}", section.label),
            ))
        }));
        items.push(MenuItem::new(format!(
            "Toggle Adaptive component [{}]",
            status_label(adaptive_component_enabled(&config))
        )));
        items.extend(adaptive_fields.iter().map(|field| {
            MenuItem::new(configured_label(
                config_field_configured(&adaptive, *field).unwrap_or(false),
                format!("Edit Adaptive {}", field.label),
            ))
        }));
        items.push(MenuItem::new(shortcut_label("Preview TOML", "p")));
        items.push(MenuItem::new(shortcut_label(
            format!("Save to {}", path.display()),
            "s",
        )));
        items.push(MenuItem::new(shortcut_label("Cancel", "q")));
        println!();
        println!("Observability: {summary}");
        println!("Adaptive: {adaptive_summary}");
        let observability_start_index = 1;
        let observability_end_index = observability_start_index + observability_fields.len();
        let adaptive_toggle_index = observability_end_index;
        let adaptive_start_index = adaptive_toggle_index + 1;
        let preview_index = adaptive_start_index + adaptive_fields.len();
        let save_index = preview_index + 1;
        let cancel_index = preview_index + 2;
        let selection = prompt_menu(&theme, "plugins.toml", &items, selected_index)?;
        if let Some(selected) = menu_response_index(&selection) {
            selected_index = selected;
        }
        match selection {
            MenuResponse::Selected(0) => {
                let enabled = !component_enabled(&config);
                set_component_enabled(&mut config, enabled);
            }
            MenuResponse::Selected(selection)
                if (observability_start_index..observability_end_index).contains(&selection) =>
            {
                edit_section(
                    &theme,
                    &mut observability,
                    observability_fields[selection - observability_start_index],
                )?
            }
            MenuResponse::Selected(selection) if selection == adaptive_toggle_index => {
                let enabled = !adaptive_component_enabled(&config);
                set_adaptive_component_enabled(&mut config, enabled);
            }
            MenuResponse::Selected(selection)
                if (adaptive_start_index..preview_index).contains(&selection) =>
            {
                edit_config_field(
                    &theme,
                    &mut adaptive,
                    adaptive_fields[selection - adaptive_start_index],
                )?
            }
            MenuResponse::Selected(selection) if selection == preview_index => {
                let preview_config = config_with_components(&config, &observability, &adaptive)?;
                print_preview(&preview_config)?;
            }
            MenuResponse::Selected(selection) if selection == save_index => {
                store_observability_config(&mut config, &observability)?;
                store_adaptive_config(&mut config, &adaptive)?;
                validate_config(&config)?;
                write_plugin_config(&path, &config)?;
                print_save_success(&path);
                return Ok(());
            }
            MenuResponse::Selected(selection) if selection == cancel_index => {
                return Err(CliError::Config(
                    "plugin edit cancelled; no config saved".into(),
                ));
            }
            MenuResponse::Shortcut(MenuShortcut::Preview, _) => {
                let preview_config = config_with_components(&config, &observability, &adaptive)?;
                print_preview(&preview_config)?;
            }
            MenuResponse::Shortcut(MenuShortcut::Save, _) => {
                store_observability_config(&mut config, &observability)?;
                store_adaptive_config(&mut config, &adaptive)?;
                validate_config(&config)?;
                write_plugin_config(&path, &config)?;
                print_save_success(&path);
                return Ok(());
            }
            MenuResponse::Shortcut(MenuShortcut::Help, _) => print_editor_help(),
            MenuResponse::Shortcut(MenuShortcut::Reset | MenuShortcut::Clear, selected) => {
                if (observability_start_index..observability_end_index).contains(&selected) {
                    reset_config_field(
                        &mut observability,
                        observability_fields[selected - observability_start_index],
                    )?;
                } else if (adaptive_start_index..preview_index).contains(&selected) {
                    reset_config_field(
                        &mut adaptive,
                        adaptive_fields[selected - adaptive_start_index],
                    )?;
                } else {
                    println!(
                        "  Select an Observability or Adaptive field, or a section field, to reset or clear."
                    );
                }
            }
            MenuResponse::Cancel | MenuResponse::Selected(_) => {
                return Err(CliError::Config(
                    "plugin edit cancelled; no config saved".into(),
                ));
            }
        }
    }
}

fn menu_response_index(response: &MenuResponse) -> Option<usize> {
    match response {
        MenuResponse::Selected(index)
        | MenuResponse::Shortcut(
            MenuShortcut::Preview
            | MenuShortcut::Save
            | MenuShortcut::Help
            | MenuShortcut::Reset
            | MenuShortcut::Clear,
            index,
        ) => Some(*index),
        MenuResponse::Cancel => None,
    }
}

fn prompt_menu(
    theme: &ColorfulTheme,
    prompt: &str,
    items: &[MenuItem],
    default: usize,
) -> Result<MenuResponse, CliError> {
    if items.is_empty() {
        return Err(CliError::Config(format!("{prompt} menu has no items")));
    }
    let term = Term::stderr();
    let mut selected = default.min(items.len() - 1);
    let mut rendered_lines = 0;
    loop {
        if rendered_lines > 0 {
            term.clear_last_lines(rendered_lines).map_err(menu_error)?;
        }
        let lines = render_menu(theme, prompt, items, selected);
        rendered_lines = lines.len();
        for line in &lines {
            term.write_line(line).map_err(menu_error)?;
        }
        term.flush().map_err(menu_error)?;
        match term.read_key().map_err(menu_error)? {
            Key::ArrowUp | Key::Char('k') => {
                selected = if selected == 0 {
                    items.len() - 1
                } else {
                    selected - 1
                };
            }
            Key::ArrowDown | Key::Char('j') => {
                selected = (selected + 1) % items.len();
            }
            Key::Enter | Key::Char(' ') => {
                clear_menu(&term, rendered_lines)?;
                return Ok(MenuResponse::Selected(selected));
            }
            Key::Char('p') => {
                clear_menu(&term, rendered_lines)?;
                return Ok(MenuResponse::Shortcut(MenuShortcut::Preview, selected));
            }
            Key::Char('s') => {
                clear_menu(&term, rendered_lines)?;
                return Ok(MenuResponse::Shortcut(MenuShortcut::Save, selected));
            }
            Key::Char('r') => {
                clear_menu(&term, rendered_lines)?;
                return Ok(MenuResponse::Shortcut(MenuShortcut::Reset, selected));
            }
            Key::Backspace | Key::Del => {
                clear_menu(&term, rendered_lines)?;
                return Ok(MenuResponse::Shortcut(MenuShortcut::Clear, selected));
            }
            Key::Char('?') => {
                clear_menu(&term, rendered_lines)?;
                return Ok(MenuResponse::Shortcut(MenuShortcut::Help, selected));
            }
            Key::Escape | Key::CtrlC | Key::Char('q') => {
                clear_menu(&term, rendered_lines)?;
                return Ok(MenuResponse::Cancel);
            }
            _ => {}
        }
    }
}

fn render_menu(
    theme: &ColorfulTheme,
    prompt: &str,
    items: &[MenuItem],
    selected: usize,
) -> Vec<String> {
    let mut lines = Vec::with_capacity(items.len() + 2);
    lines.push(format!(
        "{} {} {}",
        theme.prompt_prefix,
        theme.prompt_style.apply_to(prompt),
        theme.prompt_suffix
    ));
    lines.push(
        theme
            .hint_style
            .apply_to("  ↑/↓ or j/k move, Enter/Space select, p preview, s save, r reset, Backspace/Delete clear, ? help, q cancel.")
            .to_string(),
    );
    lines.extend(items.iter().enumerate().map(|(index, item)| {
        if index == selected {
            format!(
                "{} {}",
                theme.active_item_prefix,
                theme.active_item_style.apply_to(&item.label)
            )
        } else {
            format!(
                "{} {}",
                theme.inactive_item_prefix,
                theme.inactive_item_style.apply_to(&item.label)
            )
        }
    }));
    lines
}

fn clear_menu(term: &Term, rendered_lines: usize) -> Result<(), CliError> {
    if rendered_lines > 0 {
        term.clear_last_lines(rendered_lines).map_err(menu_error)?;
    }
    Ok(())
}

fn menu_error(error: std::io::Error) -> CliError {
    if matches!(
        error.kind(),
        std::io::ErrorKind::Interrupted | std::io::ErrorKind::UnexpectedEof
    ) {
        CliError::Config("plugin edit cancelled; no config saved".into())
    } else {
        CliError::Config(format!("plugin editor terminal error: {error}"))
    }
}

fn print_editor_help() {
    println!();
    println!(
        "{} {}",
        style("?").yellow(),
        style("Plugin editor keys").bold()
    );
    println!("  {}  move", style("↑/↓ or j/k").cyan());
    println!(
        "  {} select/toggle the highlighted item",
        style("Enter/Space").cyan()
    );
    println!(
        "  {}             reset the highlighted field or section",
        style("r").cyan()
    );
    println!(
        "  {} clear the highlighted optional field",
        style("Backspace/Del").cyan()
    );
    println!(
        "  {}             preview TOML from the main menu",
        style("p").cyan()
    );
    println!(
        "  {}             save from the main menu",
        style("s").cyan()
    );
    println!("  {}      go back/cancel", style("q or Esc").cyan());
}

fn ensure_tty() -> Result<(), CliError> {
    if !std::io::stdin().is_terminal()
        || !std::io::stdout().is_terminal()
        || !std::io::stderr().is_terminal()
    {
        return Err(CliError::Config(
            "interactive plugin editing requires a TTY".into(),
        ));
    }
    Ok(())
}

fn edit_section<T>(
    theme: &ColorfulTheme,
    config: &mut T,
    section: EditorFieldSpec,
) -> Result<(), CliError>
where
    T: SerializeConfig,
{
    let fields = section
        .schema()
        .ok_or_else(|| CliError::Config(format!("{} is not an editable section", section.name)))?
        .fields;
    let mut selected_index = 0;
    loop {
        let items = section_menu_items(config, section, fields)?;
        let selection = prompt_menu(theme, section.name, &items, selected_index)?;
        if let Some(selected) = menu_response_index(&selection) {
            selected_index = selected;
        }
        let selection = match selection {
            MenuResponse::Selected(selection) => selection,
            MenuResponse::Shortcut(MenuShortcut::Help, _) => {
                print_editor_help();
                continue;
            }
            MenuResponse::Shortcut(MenuShortcut::Reset, selected) => {
                reset_selected_item(config, section, fields, selected)?;
                continue;
            }
            MenuResponse::Shortcut(MenuShortcut::Clear, selected) => {
                if reset_selected_field(config, section, fields, selected)? {
                    continue;
                }
                println!("  Select a field to clear.");
                continue;
            }
            MenuResponse::Shortcut(MenuShortcut::Preview | MenuShortcut::Save, _) => {
                println!("  Preview and save are available from the main plugins.toml menu.");
                continue;
            }
            MenuResponse::Cancel => return Ok(()),
        };
        if !edit_selected_section_item(theme, config, section, fields, selection)? {
            return Ok(());
        }
    }
}

fn section_menu_items<T>(
    config: &T,
    section: EditorFieldSpec,
    fields: &[EditorFieldSpec],
) -> Result<Vec<MenuItem>, CliError>
where
    T: serde::Serialize,
{
    let mut items = Vec::new();
    if section_has_enabled_toggle(section) {
        let enabled = section_enabled(config, section).unwrap_or(false);
        items.push(MenuItem::new(format!(
            "Toggle section [{}]",
            status_label(enabled)
        )));
    }
    for field in fields {
        items.push(section_field_menu_item(config, section, *field)?);
    }
    items.push(MenuItem::new(shortcut_label("Reset section", "r")));
    items.push(MenuItem::new(shortcut_label("Back", "q")));
    Ok(items)
}

fn section_field_menu_item<T>(
    config: &T,
    section: EditorFieldSpec,
    field: EditorFieldSpec,
) -> Result<MenuItem, CliError>
where
    T: serde::Serialize,
{
    let configured = section_field_configured(config, section, field)?;
    let value = section_field_value(config, section, field.name)?
        .map(|value| display_field_value(section, field, &value))
        .or_else(|| {
            default_field_value(section, field)
                .map(|value| format!("{} (default)", display_value(&value)))
        })
        .unwrap_or_else(|| "(default)".to_string());
    Ok(MenuItem::new(format!(
        "{} = {}",
        configured_label(configured, field.name),
        value
    )))
}

fn selected_field_index(section: EditorFieldSpec, selected: usize) -> usize {
    selected - usize::from(section_has_enabled_toggle(section))
}

fn reset_section_index(section: EditorFieldSpec, fields: &[EditorFieldSpec]) -> usize {
    usize::from(section_has_enabled_toggle(section)) + fields.len()
}

fn reset_selected_item<T>(
    config: &mut T,
    section: EditorFieldSpec,
    fields: &[EditorFieldSpec],
    selected: usize,
) -> Result<(), CliError>
where
    T: SerializeConfig,
{
    if reset_selected_field(config, section, fields, selected)? {
        return Ok(());
    }
    if selected == reset_section_index(section, fields) {
        reset_section(config, section);
    }
    Ok(())
}

fn edit_selected_section_item<T>(
    theme: &ColorfulTheme,
    config: &mut T,
    section: EditorFieldSpec,
    fields: &[EditorFieldSpec],
    selection: usize,
) -> Result<bool, CliError>
where
    T: SerializeConfig,
{
    if section_has_enabled_toggle(section) && selection == 0 {
        toggle_section(config, section);
        return Ok(true);
    }
    let index = selected_field_index(section, selection);
    if let Some(field) = fields.get(index) {
        edit_field(theme, config, section, field)?;
        return Ok(true);
    }
    if index == fields.len() {
        reset_section(config, section);
        return Ok(true);
    }
    Ok(false)
}

fn edit_field<T>(
    theme: &ColorfulTheme,
    config: &mut T,
    section: EditorFieldSpec,
    field: &EditorFieldSpec,
) -> Result<(), CliError>
where
    T: SerializeConfig,
{
    if field.kind == EditorFieldKind::Section {
        edit_nested_section(theme, config, section, *field)?;
        return Ok(());
    }
    let current = section_field_value(config, section, field.name)?;
    let actions = [
        MenuItem::new("Set value"),
        MenuItem::new(shortcut_label(
            "Reset to default/none",
            "r, Backspace, Delete",
        )),
        MenuItem::new(shortcut_label("Back", "q")),
    ];
    let action = prompt_menu(
        theme,
        &format!(
            "{}.{}, current {}",
            section.name,
            field.name,
            current
                .as_ref()
                .map(|value| display_field_value(section, *field, value))
                .unwrap_or_else(|| "(default)".to_string())
        ),
        &actions,
        0,
    )?;
    match action {
        MenuResponse::Selected(0) => {
            let value = prompt_value(theme, field, current.as_ref())?;
            set_section_field(config, section, field.name, value)?;
        }
        MenuResponse::Selected(1)
        | MenuResponse::Shortcut(MenuShortcut::Reset | MenuShortcut::Clear, _) => {
            remove_section_field(config, section, field.name)?
        }
        MenuResponse::Shortcut(MenuShortcut::Help, _) => print_editor_help(),
        MenuResponse::Shortcut(MenuShortcut::Preview | MenuShortcut::Save, _) => {
            println!("  Preview and save are available from the main plugins.toml menu.");
        }
        _ => {}
    }
    Ok(())
}

fn edit_config_field<T>(
    theme: &ColorfulTheme,
    config: &mut T,
    field: EditorFieldSpec,
) -> Result<(), CliError>
where
    T: Default + SerializeConfig,
{
    if field.kind == EditorFieldKind::Section {
        let mut value = config_field_value(config, field.name)?
            .or_else(|| field.default_value())
            .unwrap_or_else(|| json!({}));
        let schema = field.schema().ok_or_else(|| {
            CliError::Config(format!("{} is not an editable section", field.name))
        })?;
        if edit_value_section(theme, field.name, &mut value, schema, field.default_value())? {
            store_edited_config_section(config, field, value)?;
        }
        return Ok(());
    }

    let current = config_field_value(config, field.name)?;
    let actions = [
        MenuItem::new("Set value"),
        MenuItem::new(shortcut_label(
            "Reset to default/none",
            "r, Backspace, Delete",
        )),
        MenuItem::new(shortcut_label("Back", "q")),
    ];
    let action = prompt_menu(
        theme,
        &format!(
            "{}, current {}",
            field.label,
            current
                .as_ref()
                .map(display_value)
                .or_else(|| default_config_field_value::<T>(field)
                    .map(|value| { format!("{} (default)", display_value(&value)) }))
                .unwrap_or_else(|| "(default)".to_string())
        ),
        &actions,
        0,
    )?;
    match action {
        MenuResponse::Selected(0) => {
            let value = prompt_value(theme, &field, current.as_ref())?;
            set_struct_field(config, field.name, value)?;
        }
        MenuResponse::Selected(1)
        | MenuResponse::Shortcut(MenuShortcut::Reset | MenuShortcut::Clear, _) => {
            reset_config_field(config, field)?
        }
        MenuResponse::Shortcut(MenuShortcut::Help, _) => print_editor_help(),
        MenuResponse::Shortcut(MenuShortcut::Preview | MenuShortcut::Save, _) => {
            println!("  Preview and save are available from the main plugins.toml menu.");
        }
        _ => {}
    }
    Ok(())
}

fn edit_nested_section<T>(
    theme: &ColorfulTheme,
    config: &mut T,
    section: EditorFieldSpec,
    field: EditorFieldSpec,
) -> Result<(), CliError>
where
    T: SerializeConfig,
{
    let mut value = section_field_value(config, section, field.name)?
        .or_else(|| field.default_value())
        .unwrap_or_else(|| json!({}));
    let schema = field
        .schema()
        .ok_or_else(|| CliError::Config(format!("{} is not an editable section", field.name)))?;
    if edit_value_section(
        theme,
        &format!("{}.{}", section.name, field.name),
        &mut value,
        schema,
        field.default_value(),
    )? {
        store_edited_section_field(config, section, field, value)?;
    }
    Ok(())
}

fn store_edited_config_section<T>(
    config: &mut T,
    field: EditorFieldSpec,
    value: Value,
) -> Result<(), CliError>
where
    T: SerializeConfig,
{
    if should_clear_empty_section(field, &value) {
        remove_struct_field(config, field.name)
    } else {
        set_struct_field(config, field.name, value)
    }
}

fn store_edited_section_field<T>(
    config: &mut T,
    section: EditorFieldSpec,
    field: EditorFieldSpec,
    value: Value,
) -> Result<(), CliError>
where
    T: SerializeConfig,
{
    if should_clear_empty_section(field, &value) {
        remove_section_field(config, section, field.name)
    } else {
        set_section_field(config, section, field.name, value)
    }
}

fn edit_value_section(
    theme: &ColorfulTheme,
    prompt: &str,
    value: &mut Value,
    schema: &nemo_flow::config_editor::EditorSchema,
    default: Option<Value>,
) -> Result<bool, CliError> {
    ensure_object(value);
    let original = value.clone();
    let mut selected_index = 0;
    loop {
        let items = value_section_menu_items(value, schema, default.as_ref())?;
        let selection = prompt_menu(theme, prompt, &items, selected_index)?;
        if let Some(selected) = menu_response_index(&selection) {
            selected_index = selected;
        }
        let selection = match selection {
            MenuResponse::Selected(selection) => selection,
            MenuResponse::Shortcut(MenuShortcut::Help, _) => {
                print_editor_help();
                continue;
            }
            MenuResponse::Shortcut(MenuShortcut::Reset, selected) => {
                reset_value_section_item(value, schema, default.as_ref(), selected);
                continue;
            }
            MenuResponse::Shortcut(MenuShortcut::Clear, selected) => {
                if clear_value_field(value, schema, selected) {
                    continue;
                }
                println!("  Select a field to clear.");
                continue;
            }
            MenuResponse::Shortcut(MenuShortcut::Preview | MenuShortcut::Save, _) => {
                println!("  Preview and save are available from the main plugins.toml menu.");
                continue;
            }
            MenuResponse::Cancel => return Ok(*value != original),
        };
        if !edit_selected_value_item(theme, prompt, value, schema, default.as_ref(), selection)? {
            return Ok(*value != original);
        }
    }
}

fn value_section_menu_items(
    value: &Value,
    schema: &nemo_flow::config_editor::EditorSchema,
    default: Option<&Value>,
) -> Result<Vec<MenuItem>, CliError> {
    let mut items = schema
        .fields
        .iter()
        .map(|field| value_field_menu_item(value, *field, default))
        .collect::<Result<Vec<_>, _>>()?;
    items.push(MenuItem::new(shortcut_label("Reset section", "r")));
    items.push(MenuItem::new(shortcut_label("Back", "q")));
    Ok(items)
}

fn value_field_menu_item(
    value: &Value,
    field: EditorFieldSpec,
    default: Option<&Value>,
) -> Result<MenuItem, CliError> {
    let configured = value_field_configured(value, field, default);
    let rendered = value_field_value(value, field.name)
        .map(|value| display_value_with_default(&value, default_object_field_value(default, field)))
        .or_else(|| {
            default_object_field_value(default, field)
                .map(|value| format!("{} (default)", display_value(&value)))
        })
        .unwrap_or_else(|| "(default)".to_string());
    Ok(MenuItem::new(format!(
        "{} = {}",
        configured_label(configured, field.name),
        rendered
    )))
}

fn edit_selected_value_item(
    theme: &ColorfulTheme,
    prompt: &str,
    value: &mut Value,
    schema: &nemo_flow::config_editor::EditorSchema,
    default: Option<&Value>,
    selection: usize,
) -> Result<bool, CliError> {
    if let Some(field) = schema.fields.get(selection) {
        edit_value_field(theme, prompt, value, *field, default)?;
        return Ok(true);
    }
    if selection == schema.fields.len() {
        *value = default.cloned().unwrap_or_else(|| json!({}));
        ensure_object(value);
        return Ok(true);
    }
    Ok(false)
}

fn edit_value_field(
    theme: &ColorfulTheme,
    prompt: &str,
    value: &mut Value,
    field: EditorFieldSpec,
    default: Option<&Value>,
) -> Result<(), CliError> {
    if field.kind == EditorFieldKind::Section {
        let mut nested_value = value_field_value(value, field.name)
            .or_else(|| field.default_value())
            .unwrap_or_else(|| json!({}));
        let nested_schema = field.schema().ok_or_else(|| {
            CliError::Config(format!("{} is not an editable section", field.name))
        })?;
        if edit_value_section(
            theme,
            &format!("{prompt}.{}", field.name),
            &mut nested_value,
            nested_schema,
            field.default_value(),
        )? {
            store_edited_value_section(value, field, nested_value);
        }
        return Ok(());
    }

    let current = value_field_value(value, field.name);
    let actions = [
        MenuItem::new("Set value"),
        MenuItem::new(shortcut_label(
            "Reset to default/none",
            "r, Backspace, Delete",
        )),
        MenuItem::new(shortcut_label("Back", "q")),
    ];
    let action = prompt_menu(
        theme,
        &format!(
            "{prompt}.{}, current {}",
            field.name,
            current
                .as_ref()
                .map(|value| {
                    display_value_with_default(value, default_object_field_value(default, field))
                })
                .or_else(|| {
                    default_object_field_value(default, field)
                        .map(|value| format!("{} (default)", display_value(&value)))
                })
                .unwrap_or_else(|| "(default)".to_string())
        ),
        &actions,
        0,
    )?;
    match action {
        MenuResponse::Selected(0) => {
            let field_value = prompt_value(theme, &field, current.as_ref())?;
            set_value_field(value, field.name, field_value);
        }
        MenuResponse::Selected(1)
        | MenuResponse::Shortcut(MenuShortcut::Reset | MenuShortcut::Clear, _) => {
            reset_value_field(value, field, default)
        }
        MenuResponse::Shortcut(MenuShortcut::Help, _) => print_editor_help(),
        MenuResponse::Shortcut(MenuShortcut::Preview | MenuShortcut::Save, _) => {
            println!("  Preview and save are available from the main plugins.toml menu.");
        }
        _ => {}
    }
    Ok(())
}

fn reset_value_section_item(
    value: &mut Value,
    schema: &nemo_flow::config_editor::EditorSchema,
    default: Option<&Value>,
    selected: usize,
) {
    if let Some(field) = schema.fields.get(selected) {
        reset_value_field(value, *field, default);
    } else if selected == schema.fields.len() {
        *value = default.cloned().unwrap_or_else(|| json!({}));
        ensure_object(value);
    }
}

fn clear_value_field(
    value: &mut Value,
    schema: &nemo_flow::config_editor::EditorSchema,
    selected: usize,
) -> bool {
    let Some(field) = schema.fields.get(selected) else {
        return false;
    };
    remove_value_field(value, field.name);
    true
}

fn value_field_configured(value: &Value, field: EditorFieldSpec, default: Option<&Value>) -> bool {
    let Some(current) = value_field_value(value, field.name) else {
        return false;
    };
    if field.optional {
        return true;
    }
    default_object_field_value(default, field)
        .as_ref()
        .is_none_or(|default| default != &current)
}

fn value_field_value(value: &Value, field: &str) -> Option<Value> {
    value
        .as_object()
        .and_then(|object| object.get(field))
        .filter(|value| !value.is_null())
        .cloned()
}

fn default_object_field_value(default: Option<&Value>, field: EditorFieldSpec) -> Option<Value> {
    default
        .and_then(Value::as_object)
        .and_then(|object| object.get(field.name))
        .filter(|value| !value.is_null())
        .cloned()
}

fn set_value_field(target: &mut Value, field: &str, field_value: Value) {
    ensure_object(target).insert(field.to_string(), field_value);
}

fn store_edited_value_section(target: &mut Value, field: EditorFieldSpec, field_value: Value) {
    if should_clear_empty_section(field, &field_value) {
        remove_value_field(target, field.name);
    } else {
        set_value_field(target, field.name, field_value);
    }
}

fn remove_value_field(target: &mut Value, field: &str) {
    if let Some(object) = target.as_object_mut() {
        object.remove(field);
    }
}

fn reset_value_field(value: &mut Value, field: EditorFieldSpec, default: Option<&Value>) {
    if let Some(default) = default_object_field_value(default, field) {
        set_value_field(value, field.name, default);
    } else {
        remove_value_field(value, field.name);
    }
}

fn display_value_with_default(value: &Value, default: Option<Value>) -> String {
    if default.as_ref().is_some_and(|default| default == value) {
        format!("{} (default)", display_value(value))
    } else {
        display_value(value)
    }
}

trait SerializeConfig: serde::Serialize + serde::de::DeserializeOwned {}

impl<T> SerializeConfig for T where T: serde::Serialize + serde::de::DeserializeOwned {}

fn prompt_value(
    theme: &ColorfulTheme,
    field: &EditorFieldSpec,
    current: Option<&Value>,
) -> Result<Value, CliError> {
    match field.kind {
        EditorFieldKind::Boolean => {
            let values = ["false", "true"];
            let default_idx = current
                .and_then(Value::as_bool)
                .map(usize::from)
                .unwrap_or(0);
            let idx = Select::with_theme(theme)
                .with_prompt(field.label)
                .items(&values)
                .default(default_idx)
                .interact()
                .map_err(editor_error)?;
            Ok(json!(idx == 1))
        }
        EditorFieldKind::Integer => {
            let initial = current.map(display_value).unwrap_or_default();
            let value: String = Input::with_theme(theme)
                .with_prompt(field.label)
                .with_initial_text(initial)
                .interact_text()
                .map_err(editor_error)?;
            let parsed = value.trim().parse::<i64>().map_err(|error| {
                CliError::Config(format!("{} must be an integer: {error}", field.name))
            })?;
            Ok(json!(parsed))
        }
        EditorFieldKind::Float => {
            let initial = current.map(display_value).unwrap_or_default();
            let value: String = Input::with_theme(theme)
                .with_prompt(field.label)
                .with_initial_text(initial)
                .interact_text()
                .map_err(editor_error)?;
            parse_float_value(field, &value)
        }
        EditorFieldKind::StringMap | EditorFieldKind::Json => {
            let initial = current.map(display_value).unwrap_or_else(|| {
                if matches!(field.name, "tool_definitions" | "learners") {
                    "[]".to_string()
                } else {
                    "{}".to_string()
                }
            });
            let value: String = Input::with_theme(theme)
                .with_prompt(format!("{} as JSON", field.label))
                .with_initial_text(initial)
                .interact_text()
                .map_err(editor_error)?;
            serde_json::from_str(value.trim()).map_err(|error| {
                CliError::Config(format!("invalid JSON for {}: {error}", field.name))
            })
        }
        EditorFieldKind::Enum => {
            let values = field.enum_values;
            let default_idx = current
                .and_then(Value::as_str)
                .and_then(|value| values.iter().position(|candidate| *candidate == value))
                .unwrap_or(0);
            let idx = Select::with_theme(theme)
                .with_prompt(field.label)
                .items(values)
                .default(default_idx)
                .interact()
                .map_err(editor_error)?;
            Ok(json!(values[idx]))
        }
        EditorFieldKind::String => {
            let initial = current.and_then(Value::as_str).unwrap_or_default();
            let value: String = Input::with_theme(theme)
                .with_prompt(field.label)
                .with_initial_text(initial)
                .interact_text()
                .map_err(editor_error)?;
            Ok(json!(value))
        }
        EditorFieldKind::Section => Err(CliError::Config(format!(
            "{} is a nested section and cannot be edited as a scalar",
            field.name
        ))),
    }
}

fn parse_float_value(field: &EditorFieldSpec, value: &str) -> Result<Value, CliError> {
    let value = value.trim();
    let parsed = value
        .parse::<f64>()
        .map_err(|error| CliError::Config(format!("{} must be a number: {error}", field.name)))?;
    if !parsed.is_finite() {
        return Err(CliError::Config(format!(
            "{} must be a finite number: {value}",
            field.name
        )));
    }
    Ok(json!(parsed))
}

fn editor_error(err: dialoguer::Error) -> CliError {
    match err {
        dialoguer::Error::IO(io_err)
            if matches!(
                io_err.kind(),
                std::io::ErrorKind::Interrupted | std::io::ErrorKind::UnexpectedEof
            ) =>
        {
            CliError::Config("plugin edit cancelled; no config saved".into())
        }
        other => CliError::Config(format!("plugin edit error: {other}")),
    }
}

#[cfg(test)]
#[path = "../tests/coverage/plugins_tests.rs"]
mod tests;
