use crate::error::AppResult;
use crate::types::state::AppState;
use crate::ui::AppBackend;
use std::path::Path;
use std::process::{Command, Stdio};

use super::reload;

/// How a subprocess command should be run.
#[derive(Debug, Clone)]
pub enum SubprocessCallMode {
    /// `shell`: run via the configured shell, taking over the terminal.
    Interactive,
    /// `spawn`: run detached, without taking over the terminal.
    Spawn,
    /// `capture`: run and capture its stdout for later use (e.g. by `stdout`).
    Capture,
}

/// Returns the selected entries (or just the current entry, if none selected) as
/// `(name, path)` pairs.
pub fn current_files(app_state: &AppState) -> Vec<(&str, &Path)> {
    let mut result = Vec::new();
    if let Some(curr_list) = app_state
        .state
        .tab_state_ref()
        .curr_tab_ref()
        .curr_list_ref()
    {
        let mut i = 0;
        curr_list.iter_selected().for_each(|entry| {
            result.push((entry.file_name(), entry.file_path()));
            i += 1;
        });
        if i == 0 {
            if let Some(entry) = curr_list.curr_entry_ref() {
                result.push((entry.file_name(), entry.file_path()));
            }
        }
    }

    result
}

fn current_dir(app_state: &AppState) -> &Path {
    app_state.state.tab_state_ref().curr_tab_ref().get_cwd()
}

fn shell_quote(val: &str) -> String {
    format!("'{}'", val.replace('\'', "'\\''"))
}

/// Builds and runs the subprocess for `words`, expanding `%s`/`%p`/`%d` placeholders (shell-quoted
/// in `Interactive` mode, as separate args otherwise), per `mode`.
fn execute_sub_process(
    app_state: &mut AppState,
    words: &[String],
    mode: SubprocessCallMode,
) -> std::io::Result<()> {
    let current_files = current_files(app_state);
    let current_dir = current_dir(app_state);

    let mut command: Command;
    if let SubprocessCallMode::Interactive = mode {
        command = Command::new(&app_state.config.shell);
        command.arg("-c");
        let files_str = current_files
            .iter()
            .map(|(n, _)| shell_quote(n))
            .collect::<Vec<String>>()
            .join(" ");
        let dirs_str = current_files
            .iter()
            .map(|(_, p)| shell_quote(&p.to_string_lossy()))
            .collect::<Vec<String>>()
            .join(" ");
        command.arg(
            words
                .join(" ")
                .replace("%d", &shell_quote(&current_dir.to_string_lossy()))
                .replace("%s", &files_str)
                .replace("%p", &dirs_str),
        );
    } else {
        command = Command::new(words[0].clone());
        for word in words.iter().skip(1) {
            match word.as_str() {
                "%s" => {
                    for (file_name, _) in &current_files {
                        command.arg(file_name);
                    }
                }
                "%p" => {
                    for (_, file_path) in &current_files {
                        command.arg(file_path);
                    }
                }
                "%d" => {
                    command.arg(current_dir);
                }
                s => {
                    command.arg(s);
                }
            }
        }
    }

    match mode {
        SubprocessCallMode::Interactive => {
            let status = command.status();
            match status {
                Ok(status) => {
                    if status.code() == Some(0) {
                        Ok(())
                    } else {
                        Err(std::io::Error::other(format!(
                            "Command failed with {}",
                            status
                        )))
                    }
                }
                Err(err) => Err(std::io::Error::other(format!(
                    "Shell execution failed: {}",
                    err
                ))),
            }
        }
        SubprocessCallMode::Spawn => {
            command
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()?;
            Ok(())
        }
        SubprocessCallMode::Capture => {
            let output = command
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .output()?;
            if output.status.code() == Some(0) {
                app_state.state.last_stdout =
                    Some(String::from_utf8_lossy(&output.stdout).to_string());
                Ok(())
            } else {
                Err(std::io::Error::other(format!(
                    "Command failed with {}",
                    output.status
                )))
            }
        }
    }
}

/// Handler for Joshuto's `shell` and `spawn` commands.
pub fn sub_process(
    app_state: &mut AppState,
    backend: &mut AppBackend,
    words: &[String],
    mode: SubprocessCallMode,
) -> AppResult {
    match mode {
        SubprocessCallMode::Interactive => {
            // Joshuto needs to release the terminal when handing it over to some interactive
            // shell command and restore it afterwards
            backend.terminal_drop();
            let res = execute_sub_process(app_state, words, mode);
            backend.terminal_restore()?;
            let _ = reload::soft_reload_curr_tab(app_state);
            res?;
            app_state
                .state
                .message_queue_mut()
                .push_info(format!("Finished: {}", words.join(" ")));
        }
        SubprocessCallMode::Spawn => {
            execute_sub_process(app_state, words, mode)?;
            app_state
                .state
                .message_queue_mut()
                .push_info(format!("Spawned: {}", words.join(" ")));
        }
        SubprocessCallMode::Capture => {
            execute_sub_process(app_state, words, mode)?;
            let _ = reload::soft_reload_curr_tab(app_state);
        }
    };
    Ok(())
}
