use std::path::Path;
use std::time::Duration;

use anyhow::Result;
use vtcode_commons::resolve_editor_target;
use vtcode_core::config::EditorToolConfig;
use vtcode_core::config::loader::ConfigManager;
use vtcode_core::tools::terminal_app::{EditorLaunchConfig, TerminalAppLauncher};
use vtcode_core::utils::ansi::MessageStyle;
use vtcode_tui::app::{
    InlineHandle, InlineListItem, InlineListSelection, ListOverlayRequest, TransientRequest,
    TransientSubmission, WizardModalMode, WizardStep,
};

use vtcode_core::hooks::SessionEndReason;

use super::{SlashCommandContext, SlashCommandControl};
use crate::agent::runloop::unified::external_url_guard::{
    ExternalUrlGuardContext, ExternalUrlOpenOutcome, request_external_url_open,
};
use crate::agent::runloop::unified::overlay_prompt::{OverlayWaitOutcome, show_overlay_and_wait};
use crate::agent::runloop::unified::palettes::{
    ActivePalette, refresh_runtime_config_from_manager,
};
use crate::agent::runloop::unified::settings_interactive::{
    create_settings_palette_state, resolve_settings_view_path, show_settings_palette,
};
use crate::agent::runloop::unified::url_guard::open_external_url;
use crate::agent::runloop::unified::wizard_modal::{
    WizardModalOutcome, show_wizard_modal_and_wait,
};

/// Time to wait for the event stream to fully stop before launching an external editor.
/// Covers the 100ms stop timeout + processing delay in the drive loop.
const EVENT_STREAM_STOP_DELAY: Duration = Duration::from_millis(150);
const DOCS_URL: &str = "https://deepwiki.com/vinhnx/vtcode";
const DONATE_URL: &str = "https://buymeacoffee.com/vinhnx";
const PROJECT_URL: &str = "https://github.com/vinhnx/vtcode";
const EXTERNAL_EDITOR_TITLE: &str = "External Editor";
const FILE_OPENER_SETTINGS_PATH: &str = "file_opener";
const EDITOR_ENABLED_ID: &str = "tools_editor_enabled";
const EDITOR_PRESET_ID: &str = "tools_editor_preset";
const EDITOR_SUSPEND_ID: &str = "tools_editor_suspend";
const EDITOR_FOLLOW_UP_ID: &str = "tools_editor_follow_up";
const EDITOR_CUSTOM_COMMAND_ID: &str = "tools_editor_custom_command";
const WORKFLOW_ENABLED: &str = "enabled";
const WORKFLOW_DISABLED: &str = "disabled";
const WORKFLOW_DONE: &str = "done";
const WORKFLOW_FILE_OPENER: &str = "file_opener";

pub(crate) async fn handle_new_session(
    ctx: SlashCommandContext<'_>,
) -> Result<SlashCommandControl> {
    ctx.renderer
        .line(MessageStyle::Info, "Starting new session...")?;
    Ok(SlashCommandControl::BreakWithReason(
        SessionEndReason::NewSession,
    ))
}

pub(crate) async fn handle_open_docs(ctx: SlashCommandContext<'_>) -> Result<SlashCommandControl> {
    match request_external_url_open(
        ExternalUrlGuardContext::new(ctx.handle, ctx.session, ctx.ctrl_c_state, ctx.ctrl_c_notify),
        DOCS_URL,
    )
    .await?
    {
        ExternalUrlOpenOutcome::Opened => {
            ctx.renderer.line(
                MessageStyle::Info,
                &format!("Opening documentation in browser: {}", DOCS_URL),
            )?;
        }
        ExternalUrlOpenOutcome::OpenFailed(err) => {
            ctx.renderer.line(
                MessageStyle::Error,
                &format!("Failed to open browser: {}", err),
            )?;
            ctx.renderer
                .line(MessageStyle::Info, &format!("Please visit: {}", DOCS_URL))?;
        }
        ExternalUrlOpenOutcome::Cancelled => {
            ctx.renderer
                .line(MessageStyle::Info, "Cancelled opening documentation link.")?;
        }
        ExternalUrlOpenOutcome::Exit => {
            return Ok(SlashCommandControl::BreakWithReason(SessionEndReason::Exit));
        }
        ExternalUrlOpenOutcome::Unsupported => {
            ctx.renderer.line(
                MessageStyle::Error,
                "Blocked unsupported documentation link target.",
            )?;
        }
    }
    ctx.renderer.line_if_not_empty(MessageStyle::Output)?;
    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_open_donate_links(
    ctx: SlashCommandContext<'_>,
) -> Result<SlashCommandControl> {
    let request = TransientRequest::List(ListOverlayRequest {
        title: "Support VT Code".to_string(),
        lines: vec![
            "Choose a link to open in your browser.".to_string(),
            format!("Coffee: {DONATE_URL}"),
            format!("Project: {PROJECT_URL}"),
        ],
        footer_hint: Some("Esc cancels".to_string()),
        items: vec![
            InlineListItem {
                title: "Buy me a coffee".to_string(),
                subtitle: Some(DONATE_URL.to_string()),
                badge: Some("Support".to_string()),
                indent: 0,
                selection: Some(InlineListSelection::ConfigAction(DONATE_URL.to_string())),
                search_value: None,
            },
            InlineListItem {
                title: "Open GitHub repository".to_string(),
                subtitle: Some(PROJECT_URL.to_string()),
                badge: Some("GitHub".to_string()),
                indent: 0,
                selection: Some(InlineListSelection::ConfigAction(PROJECT_URL.to_string())),
                search_value: None,
            },
        ],
        selected: Some(InlineListSelection::ConfigAction(DONATE_URL.to_string())),
        search: None,
        hotkeys: Vec::new(),
    });

    let outcome = show_overlay_and_wait(
        ctx.handle,
        ctx.session,
        request,
        ctx.ctrl_c_state,
        ctx.ctrl_c_notify,
        |submission| match submission {
            TransientSubmission::Selection(InlineListSelection::ConfigAction(url))
                if url == DONATE_URL || url == PROJECT_URL =>
            {
                Some(url)
            }
            _ => None,
        },
    )
    .await?;

    ctx.handle.close_modal();
    ctx.handle.force_redraw();

    match outcome {
        OverlayWaitOutcome::Submitted(url) => match open_external_url(&url) {
            Ok(()) => ctx
                .renderer
                .line(MessageStyle::Info, &format!("Opening browser: {url}"))?,
            Err(err) => ctx.renderer.line(
                MessageStyle::Error,
                &format!("Failed to open browser: {err}"),
            )?,
        },
        OverlayWaitOutcome::Exit => {
            return Ok(SlashCommandControl::BreakWithReason(SessionEndReason::Exit));
        }
        OverlayWaitOutcome::Cancelled | OverlayWaitOutcome::Interrupted => {
            ctx.renderer
                .line(MessageStyle::Info, "Cancelled opening support link.")?;
        }
    }

    ctx.renderer.line_if_not_empty(MessageStyle::Output)?;
    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_launch_editor(
    ctx: SlashCommandContext<'_>,
    file: Option<String>,
) -> Result<SlashCommandControl> {
    let mut ctx = ctx;
    launch_editor_from_context(&mut ctx, file).await
}

pub(crate) async fn launch_editor_from_context(
    ctx: &mut SlashCommandContext<'_>,
    file: Option<String>,
) -> Result<SlashCommandControl> {
    let launcher = TerminalAppLauncher::new(ctx.config.workspace.clone());
    let editor_config = ctx
        .vt_cfg
        .as_ref()
        .map(|config| config.tools.editor.clone())
        .unwrap_or_default();
    if !editor_config.enabled {
        ctx.renderer.line(
            MessageStyle::Warning,
            "External editor is disabled (`tools.editor.enabled = false`).",
        )?;
        ctx.renderer.line_if_not_empty(MessageStyle::Output)?;
        return Ok(SlashCommandControl::Continue);
    }

    let file_target = match file.as_deref() {
        Some(value) => match resolve_editor_target(value, &ctx.config.workspace) {
            Some(target) => Some(target),
            None => {
                ctx.renderer.line(
                    MessageStyle::Error,
                    &format!("Invalid file target for `/edit`: {value}"),
                )?;
                ctx.renderer.line_if_not_empty(MessageStyle::Output)?;
                return Ok(SlashCommandControl::Continue);
            }
        },
        None => None,
    };

    let opening_existing_file = file_target.is_some();
    let wait_for_editor = should_wait_for_editor(opening_existing_file, &editor_config);
    let is_transient_open = opening_existing_file && !wait_for_editor;

    if !is_transient_open {
        ctx.renderer.line(
            MessageStyle::Info,
            if opening_existing_file {
                "Launching editor, with the existing file, close the tab or editor to continue on the VT Code session..."
            } else {
                "Launching editor with current input..."
            },
        )?;
    }

    let launch_config = launch_config_from_settings(&editor_config, wait_for_editor);

    let launch_result = run_with_event_loop_suspended(
        ctx.handle,
        editor_config.suspend_tui && wait_for_editor,
        || launcher.launch_editor_target_with_config(file_target, launch_config),
    )
    .await;

    if launch_result.is_ok() && is_transient_open {
        ctx.handle.force_redraw();
        return Ok(SlashCommandControl::Continue);
    }

    let (message_style, message) = match launch_result {
        Ok(Some(edited_content)) => {
            ctx.handle.set_input(edited_content);
            (
                MessageStyle::Info,
                "Editor closed. Input updated with edited content.".to_owned(),
            )
        }
        Ok(None) => (MessageStyle::Info, "Editor closed.".to_owned()),
        Err(err) => (
            MessageStyle::Error,
            format!("Failed to launch editor: {}", err),
        ),
    };

    ctx.handle.force_redraw();
    ctx.renderer.line(message_style, &message)?;
    ctx.renderer.line_if_not_empty(MessageStyle::Output)?;
    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_configure_editor(
    ctx: &mut SlashCommandContext<'_>,
) -> Result<SlashCommandControl> {
    let editor_config = ctx
        .vt_cfg
        .as_ref()
        .map(|config| config.tools.editor.clone())
        .unwrap_or_default();
    let current_preset = EditorPreset::from_saved(&editor_config.preferred_editor);

    let steps = build_editor_config_steps(&editor_config, current_preset);
    let outcome = show_wizard_modal_and_wait(
        ctx.handle,
        ctx.session,
        EXTERNAL_EDITOR_TITLE.to_string(),
        steps,
        0,
        None,
        WizardModalMode::MultiStep,
        ctx.ctrl_c_state,
        ctx.ctrl_c_notify,
    )
    .await?;

    let WizardModalOutcome::Submitted(selections) = outcome else {
        return Ok(SlashCommandControl::Continue);
    };
    let Some(mut choices) = parse_editor_workflow_answers(&selections) else {
        return Ok(SlashCommandControl::Continue);
    };

    if choices.preset == EditorPreset::Custom {
        let custom_command = prompt_for_custom_editor_command(ctx, &editor_config).await?;
        let Some(custom_command) = custom_command else {
            return Ok(SlashCommandControl::Continue);
        };
        choices.custom_command = Some(custom_command);
    }

    let open_file_opener_settings = choices.open_file_opener_settings;
    persist_editor_workflow_choices(&ctx.config.workspace, choices)?;
    refresh_runtime_config_from_manager(
        ctx.renderer,
        ctx.handle,
        ctx.config,
        ctx.vt_cfg,
        ctx.provider_client.as_ref(),
        ctx.session_bootstrap,
        ctx.full_auto,
    )
    .await?;

    ctx.renderer
        .line(MessageStyle::Info, "Saved external editor settings.")?;
    ctx.renderer.line(
        MessageStyle::Output,
        "Use `/config file_opener` if you also want terminal hyperlinks to target a specific editor URI scheme.",
    )?;
    ctx.renderer.line_if_not_empty(MessageStyle::Output)?;

    if open_file_opener_settings {
        let workspace_path = ctx.config.workspace.clone();
        let vt_snapshot = ctx.vt_cfg.clone();
        let mut settings_state = create_settings_palette_state(&workspace_path, &vt_snapshot)?;
        settings_state.view_path = Some(resolve_settings_view_path(FILE_OPENER_SETTINGS_PATH));
        if show_settings_palette(ctx.renderer, &settings_state, None)? {
            *ctx.palette_state = Some(ActivePalette::Settings {
                state: Box::new(settings_state),
                esc_armed: false,
            });
        }
    }

    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_launch_git(ctx: SlashCommandContext<'_>) -> Result<SlashCommandControl> {
    use vtcode_core::tools::terminal_app::TerminalAppLauncher;

    let launcher = TerminalAppLauncher::new(ctx.config.workspace.clone());

    ctx.renderer
        .line(MessageStyle::Info, "Launching git interface (lazygit)...")?;

    let (message_style, message) =
        match run_with_event_loop_suspended(ctx.handle, true, || launcher.launch_git_interface())
            .await
        {
            Ok(()) => (MessageStyle::Info, "Git interface closed.".to_owned()),
            Err(err) => (
                MessageStyle::Error,
                format!("Failed to launch git interface: {}", err),
            ),
        };

    ctx.handle.force_redraw();
    ctx.renderer.line(message_style, &message)?;
    ctx.renderer.line_if_not_empty(MessageStyle::Output)?;
    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn run_with_event_loop_suspended<T, F>(
    handle: &InlineHandle,
    suspend_tui: bool,
    launch: F,
) -> T
where
    F: FnOnce() -> T,
{
    if suspend_tui {
        // Fully stop the background EventStream task so it stops reading stdin.
        // The editor (e.g., nvim) must have exclusive access to the terminal.
        handle.stop_event_stream();
        // Wait for the stop to complete (100ms timeout in EventStreamController::stop()
        // plus margin for command delivery through the event loop).
        tokio::time::sleep(EVENT_STREAM_STOP_DELAY).await;
        handle.clear_input_queue();
    }

    let result = launch();

    if suspend_tui {
        handle.clear_input_queue();
        // Resume event processing and restart the EventStream so the TUI
        // can receive keyboard input again.
        handle.resume_event_loop();
        handle.start_event_stream();
    }

    result
}

fn should_wait_for_editor(opening_existing_file: bool, editor_config: &EditorToolConfig) -> bool {
    if !opening_existing_file {
        return true;
    }

    editor_config.suspend_tui && editor_likely_requires_terminal(editor_config)
}

fn editor_likely_requires_terminal(editor_config: &EditorToolConfig) -> bool {
    editor_command_from_settings(editor_config)
        .as_deref()
        .is_some_and(editor_command_requires_terminal)
}

fn editor_command_from_settings(editor_config: &EditorToolConfig) -> Option<String> {
    if !editor_config.preferred_editor.trim().is_empty() {
        return Some(editor_config.preferred_editor.trim().to_string());
    }

    ["VISUAL", "EDITOR"]
        .into_iter()
        .find_map(|key| std::env::var(key).ok())
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn editor_command_requires_terminal(command: &str) -> bool {
    let Some(program) = shell_words::split(command)
        .ok()
        .and_then(|tokens| tokens.first().cloned())
    else {
        return false;
    };
    let normalized = Path::new(&program)
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(&program)
        .to_ascii_lowercase();

    matches!(
        normalized.as_str(),
        "vi" | "vim" | "nvim" | "nano" | "emacs" | "pico" | "hx" | "helix"
    )
}

fn launch_config_from_settings(
    editor_config: &EditorToolConfig,
    wait_for_editor: bool,
) -> EditorLaunchConfig {
    EditorLaunchConfig {
        preferred_editor: (!editor_config.preferred_editor.trim().is_empty())
            .then(|| editor_config.preferred_editor.clone()),
        wait_for_editor,
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EditorPreset {
    Auto,
    Vscode,
    Zed,
    Neovim,
    Vim,
    SublimeText,
    Custom,
}

const EDITOR_PRESET_CHOICES: [EditorPreset; 7] = [
    EditorPreset::Auto,
    EditorPreset::Vscode,
    EditorPreset::Zed,
    EditorPreset::Neovim,
    EditorPreset::Vim,
    EditorPreset::SublimeText,
    EditorPreset::Custom,
];

impl EditorPreset {
    fn from_saved(preferred_editor: &str) -> Self {
        let trimmed = preferred_editor.trim();
        if trimmed.is_empty() {
            return Self::Auto;
        }

        let program = trimmed.split_whitespace().next().unwrap_or_default();
        let program = Path::new(program)
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or(trimmed)
            .to_ascii_lowercase();

        match program.as_str() {
            "code" | "code-insiders" => Self::Vscode,
            "zed" => Self::Zed,
            "nvim" => Self::Neovim,
            "vim" | "vi" => Self::Vim,
            "subl" => Self::SublimeText,
            _ => Self::Custom,
        }
    }

    fn label(self) -> &'static str {
        match self {
            Self::Auto => "Auto (VS Code first)",
            Self::Vscode => "VS Code",
            Self::Zed => "Zed",
            Self::Neovim => "Neovim",
            Self::Vim => "Vim",
            Self::SublimeText => "Sublime Text",
            Self::Custom => "Custom",
        }
    }

    fn value(self) -> &'static str {
        match self {
            Self::Auto => "auto",
            Self::Vscode => "vscode",
            Self::Zed => "zed",
            Self::Neovim => "neovim",
            Self::Vim => "vim",
            Self::SublimeText => "sublime_text",
            Self::Custom => "custom",
        }
    }

    fn from_value(value: &str) -> Option<Self> {
        match value {
            "auto" => Some(Self::Auto),
            "vscode" => Some(Self::Vscode),
            "zed" => Some(Self::Zed),
            "neovim" => Some(Self::Neovim),
            "vim" => Some(Self::Vim),
            "sublime_text" => Some(Self::SublimeText),
            "custom" => Some(Self::Custom),
            _ => None,
        }
    }

    fn default_command(self) -> Option<&'static str> {
        match self {
            Self::Auto | Self::Custom => None,
            Self::Vscode => Some("code --wait"),
            Self::Zed => Some("zed"),
            Self::Neovim => Some("nvim"),
            Self::Vim => Some("vim"),
            Self::SublimeText => Some("subl -w"),
        }
    }

    fn picker_subtitle(self) -> String {
        match self {
            Self::Auto => {
                "Use env vars when set; otherwise probe installed editors with VS Code first."
                    .to_string()
            }
            Self::Custom => "Enter a raw editor command in the next step.".to_string(),
            _ => format!(
                "Save `{}` as the preferred editor command.",
                self.default_command().unwrap_or_default()
            ),
        }
    }

    fn preferred_editor(self, custom_command: Option<String>) -> String {
        match self {
            Self::Auto => String::new(),
            Self::Vscode | Self::Zed | Self::Neovim | Self::Vim | Self::SublimeText => {
                self.default_command().unwrap_or_default().to_string()
            }
            Self::Custom => custom_command.unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct EditorWorkflowChoices {
    enabled: bool,
    preset: EditorPreset,
    custom_command: Option<String>,
    suspend_tui: bool,
    open_file_opener_settings: bool,
}

fn build_editor_config_steps(
    editor_config: &EditorToolConfig,
    current_preset: EditorPreset,
) -> Vec<WizardStep> {
    vec![
        WizardStep {
            title: "Enable".to_string(),
            question: format!(
                "External editor launch is currently {}.",
                if editor_config.enabled {
                    "enabled"
                } else {
                    "disabled"
                }
            ),
            items: vec![
                request_choice_item(
                    EDITOR_ENABLED_ID,
                    "Enabled",
                    "Allow `/edit` and single-click file links to open in the configured editor.",
                    WORKFLOW_ENABLED,
                ),
                request_choice_item(
                    EDITOR_ENABLED_ID,
                    "Disabled",
                    "Keep editor settings saved, but block external editor launching.",
                    WORKFLOW_DISABLED,
                ),
            ],
            completed: false,
            answer: Some(InlineListSelection::RequestUserInputAnswer {
                question_id: EDITOR_ENABLED_ID.to_string(),
                selected: vec![if editor_config.enabled {
                    WORKFLOW_ENABLED.to_string()
                } else {
                    WORKFLOW_DISABLED.to_string()
                }],
                other: None,
            }),
            allow_freeform: false,
            freeform_label: None,
            freeform_placeholder: None,
            freeform_default: None,
        },
        WizardStep {
            title: "Preset".to_string(),
            question: format!("Current editor preset: {}.", current_preset.label()),
            items: EDITOR_PRESET_CHOICES
                .into_iter()
                .map(preset_choice_item)
                .collect(),
            completed: false,
            answer: Some(InlineListSelection::RequestUserInputAnswer {
                question_id: EDITOR_PRESET_ID.to_string(),
                selected: vec![current_preset.value().to_string()],
                other: None,
            }),
            allow_freeform: false,
            freeform_label: None,
            freeform_placeholder: None,
            freeform_default: None,
        },
        WizardStep {
            title: "Suspend TUI".to_string(),
            question: format!(
                "Suspending the event loop is currently {}.",
                if editor_config.suspend_tui {
                    "on"
                } else {
                    "off"
                }
            ),
            items: vec![
                request_choice_item(
                    EDITOR_SUSPEND_ID,
                    "Suspend TUI",
                    "Pause the TUI event loop while the external editor is active.",
                    WORKFLOW_ENABLED,
                ),
                request_choice_item(
                    EDITOR_SUSPEND_ID,
                    "Keep TUI live",
                    "Do not suspend the event loop while the editor is open.",
                    WORKFLOW_DISABLED,
                ),
            ],
            completed: false,
            answer: Some(InlineListSelection::RequestUserInputAnswer {
                question_id: EDITOR_SUSPEND_ID.to_string(),
                selected: vec![if editor_config.suspend_tui {
                    WORKFLOW_ENABLED.to_string()
                } else {
                    WORKFLOW_DISABLED.to_string()
                }],
                other: None,
            }),
            allow_freeform: false,
            freeform_label: None,
            freeform_placeholder: None,
            freeform_default: None,
        },
        WizardStep {
            title: "Next".to_string(),
            question: "Do you also want to jump to terminal hyperlink settings after saving?"
                .to_string(),
            items: vec![
                request_choice_item(
                    EDITOR_FOLLOW_UP_ID,
                    "Done",
                    "Save these editor settings and return to the session.",
                    WORKFLOW_DONE,
                ),
                request_choice_item(
                    EDITOR_FOLLOW_UP_ID,
                    "Open File Opener",
                    "Save these settings, then open `/config file_opener` for terminal hyperlink behavior.",
                    WORKFLOW_FILE_OPENER,
                ),
            ],
            completed: false,
            answer: Some(InlineListSelection::RequestUserInputAnswer {
                question_id: EDITOR_FOLLOW_UP_ID.to_string(),
                selected: vec![WORKFLOW_DONE.to_string()],
                other: None,
            }),
            allow_freeform: false,
            freeform_label: None,
            freeform_placeholder: None,
            freeform_default: None,
        },
    ]
}

async fn prompt_for_custom_editor_command(
    ctx: &mut SlashCommandContext<'_>,
    editor_config: &EditorToolConfig,
) -> Result<Option<String>> {
    let placeholder = if editor_config.preferred_editor.trim().is_empty() {
        EditorPreset::Vscode
            .default_command()
            .unwrap_or_default()
            .to_string()
    } else {
        editor_config.preferred_editor.clone()
    };
    let step = build_custom_editor_command_step(placeholder);

    let outcome = show_wizard_modal_and_wait(
        ctx.handle,
        ctx.session,
        EXTERNAL_EDITOR_TITLE.to_string(),
        vec![step],
        0,
        None,
        WizardModalMode::MultiStep,
        ctx.ctrl_c_state,
        ctx.ctrl_c_notify,
    )
    .await?;

    let (value, submitted) = match outcome {
        WizardModalOutcome::Submitted(selections) => (
            selections
                .into_iter()
                .find_map(|selection| match selection {
                    InlineListSelection::RequestUserInputAnswer {
                        question_id,
                        selected,
                        other,
                    } if question_id == EDITOR_CUSTOM_COMMAND_ID => other
                        .or_else(|| selected.first().cloned())
                        .map(|value| value.trim().to_string())
                        .filter(|value| !value.is_empty()),
                    _ => None,
                }),
            true,
        ),
        WizardModalOutcome::Cancelled { .. } => (None, false),
    };

    if submitted && value.is_none() {
        ctx.renderer.line(
            MessageStyle::Warning,
            "Custom editor command was not saved because the command was empty.",
        )?;
    }

    Ok(value)
}

fn build_custom_editor_command_step(placeholder: String) -> WizardStep {
    WizardStep {
        title: "Custom command".to_string(),
        question: "Enter the raw editor command. Include any flags you want VT Code to keep using."
            .to_string(),
        items: vec![InlineListItem {
            title: "Save command".to_string(),
            subtitle: Some("Press Tab to type the command, then Enter to save.".to_string()),
            badge: None,
            indent: 0,
            selection: Some(InlineListSelection::RequestUserInputAnswer {
                question_id: EDITOR_CUSTOM_COMMAND_ID.to_string(),
                selected: vec![],
                other: Some(String::new()),
            }),
            search_value: Some("editor command custom raw command".to_string()),
        }],
        completed: false,
        answer: None,
        allow_freeform: true,
        freeform_label: Some("Editor command".to_string()),
        freeform_placeholder: Some(placeholder.clone()),
        freeform_default: Some(placeholder),
    }
}

fn parse_editor_workflow_answers(
    selections: &[InlineListSelection],
) -> Option<EditorWorkflowChoices> {
    let enabled = request_answer_value(selections, EDITOR_ENABLED_ID)?;
    let preset = EditorPreset::from_value(&request_answer_value(selections, EDITOR_PRESET_ID)?)?;
    let suspend_tui = request_answer_value(selections, EDITOR_SUSPEND_ID)?;
    let follow_up = request_answer_value(selections, EDITOR_FOLLOW_UP_ID)?;

    Some(EditorWorkflowChoices {
        enabled: enabled == WORKFLOW_ENABLED,
        preset,
        custom_command: None,
        suspend_tui: suspend_tui == WORKFLOW_ENABLED,
        open_file_opener_settings: follow_up == WORKFLOW_FILE_OPENER,
    })
}

fn preset_choice_item(preset: EditorPreset) -> InlineListItem {
    let subtitle = preset.picker_subtitle();
    request_choice_item(EDITOR_PRESET_ID, preset.label(), &subtitle, preset.value())
}

fn request_answer_value(selections: &[InlineListSelection], question_id: &str) -> Option<String> {
    selections.iter().find_map(|selection| match selection {
        InlineListSelection::RequestUserInputAnswer {
            question_id: current_id,
            selected,
            other,
        } if current_id == question_id => other
            .as_ref()
            .filter(|value| !value.trim().is_empty())
            .cloned()
            .or_else(|| selected.first().cloned()),
        _ => None,
    })
}

fn request_choice_item(
    question_id: &str,
    title: &str,
    subtitle: &str,
    value: &str,
) -> InlineListItem {
    InlineListItem {
        title: title.to_string(),
        subtitle: Some(subtitle.to_string()),
        badge: None,
        indent: 0,
        selection: Some(InlineListSelection::RequestUserInputAnswer {
            question_id: question_id.to_string(),
            selected: vec![value.to_string()],
            other: None,
        }),
        search_value: Some(format!("{title} {subtitle} {value}")),
    }
}

fn persist_editor_workflow_choices(workspace: &Path, choices: EditorWorkflowChoices) -> Result<()> {
    let mut manager = ConfigManager::load_from_workspace(workspace)?;
    let mut config = manager.config().clone();
    config.tools.editor.enabled = choices.enabled;
    config.tools.editor.preferred_editor = choices.preset.preferred_editor(choices.custom_command);
    config.tools.editor.suspend_tui = choices.suspend_tui;
    manager.save_config(&config)?;
    Ok(())
}

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

    #[test]
    fn editor_preset_detects_known_commands() {
        assert_eq!(EditorPreset::from_saved(""), EditorPreset::Auto);
        assert_eq!(
            EditorPreset::from_saved("code --wait"),
            EditorPreset::Vscode
        );
        assert_eq!(EditorPreset::from_saved("zed"), EditorPreset::Zed);
        assert_eq!(EditorPreset::from_saved("nvim"), EditorPreset::Neovim);
        assert_eq!(EditorPreset::from_saved("vim"), EditorPreset::Vim);
        assert_eq!(
            EditorPreset::from_saved("subl -w"),
            EditorPreset::SublimeText
        );
        assert_eq!(
            EditorPreset::from_saved("/opt/custom/editor --flag"),
            EditorPreset::Custom
        );
    }

    #[test]
    fn parse_editor_workflow_answers_extracts_choices() {
        let selections = vec![
            InlineListSelection::RequestUserInputAnswer {
                question_id: EDITOR_ENABLED_ID.to_string(),
                selected: vec!["enabled".to_string()],
                other: None,
            },
            InlineListSelection::RequestUserInputAnswer {
                question_id: EDITOR_PRESET_ID.to_string(),
                selected: vec!["custom".to_string()],
                other: None,
            },
            InlineListSelection::RequestUserInputAnswer {
                question_id: EDITOR_SUSPEND_ID.to_string(),
                selected: vec!["disabled".to_string()],
                other: None,
            },
            InlineListSelection::RequestUserInputAnswer {
                question_id: EDITOR_FOLLOW_UP_ID.to_string(),
                selected: vec!["file_opener".to_string()],
                other: None,
            },
        ];

        assert_eq!(
            parse_editor_workflow_answers(&selections),
            Some(EditorWorkflowChoices {
                enabled: true,
                preset: EditorPreset::Custom,
                custom_command: None,
                suspend_tui: false,
                open_file_opener_settings: true,
            })
        );
    }

    #[test]
    fn persist_editor_workflow_choices_writes_preferred_editor_fields() {
        let workspace = tempdir().expect("workspace");

        persist_editor_workflow_choices(
            workspace.path(),
            EditorWorkflowChoices {
                enabled: true,
                preset: EditorPreset::Vscode,
                custom_command: None,
                suspend_tui: true,
                open_file_opener_settings: false,
            },
        )
        .expect("persist editor workflow choices");

        let manager =
            ConfigManager::load_from_workspace(workspace.path()).expect("load saved config");
        let config = manager.config();
        assert!(config.tools.editor.enabled);
        assert_eq!(config.tools.editor.preferred_editor, "code --wait");
        assert!(config.tools.editor.suspend_tui);
    }

    #[test]
    fn should_wait_for_editor_only_blocks_live_file_opens_for_terminal_editors() {
        let mut config = EditorToolConfig {
            suspend_tui: true,
            preferred_editor: "nvim".to_string(),
            ..EditorToolConfig::default()
        };

        assert!(should_wait_for_editor(false, &config));
        assert!(should_wait_for_editor(true, &config));

        config.preferred_editor = "code --wait".to_string();
        assert!(!should_wait_for_editor(true, &config));

        config.suspend_tui = false;
        config.preferred_editor = "nvim".to_string();
        assert!(!should_wait_for_editor(true, &config));
    }

    #[test]
    fn editor_command_requires_terminal_detects_known_terminal_editors() {
        assert!(editor_command_requires_terminal("nvim"));
        assert!(editor_command_requires_terminal("/usr/bin/vim"));
        assert!(editor_command_requires_terminal("helix"));
        assert!(!editor_command_requires_terminal("code --wait"));
        assert!(!editor_command_requires_terminal("zed"));
        assert!(!editor_command_requires_terminal(""));
    }

    #[test]
    fn custom_editor_command_step_uses_placeholder_as_default() {
        let step = build_custom_editor_command_step("code --wait".to_string());

        assert_eq!(step.freeform_placeholder.as_deref(), Some("code --wait"));
        assert_eq!(step.freeform_default.as_deref(), Some("code --wait"));
    }
}
