use crate::app::STOPWATCH;
use crate::line_editor::configure_ctrl_c;
use nu_ansi_term::Color;
use nu_engine::{maybe_print_errors, run_block, script::run_script_standalone, EvaluationContext};

#[allow(unused_imports)]
pub(crate) use nu_engine::script::{process_script, LineResult};

#[cfg(feature = "rustyline-support")]
use crate::line_editor::{
    configure_rustyline_editor, convert_rustyline_result_to_string,
    default_rustyline_editor_configuration, nu_line_editor_helper,
};

#[allow(unused_imports)]
use nu_data::config;
use nu_source::{Tag, Text};
use nu_stream::InputStream;
#[allow(unused_imports)]
use std::sync::atomic::Ordering;

#[cfg(feature = "rustyline-support")]
use rustyline::{self, error::ReadlineError};

use nu_errors::ShellError;
use nu_parser::ParserScope;
use nu_path::expand_tilde;
use nu_protocol::{hir::ExternalRedirection, ConfigPath, UntaggedValue, Value};

use log::trace;
use std::error::Error;
use std::iter::Iterator;
use std::path::PathBuf;

// Name of environment variable where the prompt could be stored
#[cfg(feature = "rustyline-support")]
const PROMPT_COMMAND: &str = "PROMPT_COMMAND";

pub fn search_paths() -> Vec<std::path::PathBuf> {
    use std::env;
    let mut search_paths = Vec::new();

    // Automatically add path `nu` is in as a search path
    if let Ok(exe_path) = env::current_exe() {
        if let Some(exe_dir) = exe_path.parent() {
            search_paths.push(exe_dir.to_path_buf());
        }
    }

    if let Ok(config) = nu_data::config::config(Tag::unknown()) {
        if let Some(Value {
            value: UntaggedValue::Table(pipelines),
            ..
        }) = config.get("plugin_dirs")
        {
            for pipeline in pipelines {
                if let Ok(plugin_dir) = pipeline.as_string() {
                    search_paths.push(expand_tilde(plugin_dir));
                }
            }
        }
    }

    search_paths
}

pub fn run_script_file(
    context: EvaluationContext,
    options: super::app::CliOptions,
) -> Result<(), ShellError> {
    if let Some(cfg) = options.config {
        load_cfg_as_global_cfg(&context, PathBuf::from(cfg));
    } else {
        load_global_cfg(&context);
    }

    let _ = register_plugins(&context);
    let _ = configure_ctrl_c(&context);

    let script = options
        .scripts
        .get(0)
        .ok_or_else(|| ShellError::unexpected("Nu source code not available"))?;

    run_script_standalone(script.get_code().to_string(), options.stdin, &context, true)?;

    Ok(())
}

#[cfg(feature = "rustyline-support")]
fn default_prompt_string(cwd: &str) -> String {
    format!(
        "{}{}{}{}{}{}> ",
        Color::Green.bold().prefix().to_string(),
        cwd,
        nu_ansi_term::ansi::RESET,
        Color::Cyan.bold().prefix().to_string(),
        current_branch(),
        nu_ansi_term::ansi::RESET
    )
}

#[cfg(feature = "rustyline-support")]
fn evaluate_prompt_string(prompt_line: &str, context: &EvaluationContext, cwd: &str) -> String {
    context.scope.enter_scope();
    let (prompt_block, err) = nu_parser::parse(prompt_line, 0, &context.scope);

    if err.is_some() {
        context.scope.exit_scope();
        default_prompt_string(cwd)
    } else {
        let run_result = run_block(
            &prompt_block,
            context,
            InputStream::empty(),
            ExternalRedirection::Stdout,
        );
        context.scope.exit_scope();

        match run_result {
            Ok(result) => match result.collect_string(Tag::unknown()) {
                Ok(string_result) => {
                    let errors = context.get_errors();
                    maybe_print_errors(context, Text::from(prompt_line));
                    context.clear_errors();

                    if !errors.is_empty() {
                        "> ".into()
                    } else {
                        string_result.item
                    }
                }
                Err(e) => {
                    context.host().lock().print_err(e, &Text::from(prompt_line));
                    context.clear_errors();

                    "> ".into()
                }
            },
            Err(e) => {
                context.host().lock().print_err(e, &Text::from(prompt_line));
                context.clear_errors();

                "> ".into()
            }
        }
    }
}

#[cfg(feature = "rustyline-support")]
pub fn cli(
    context: EvaluationContext,
    options: super::app::CliOptions,
) -> Result<(), Box<dyn Error>> {
    let _ = configure_ctrl_c(&context);

    // start time for running startup scripts (this metric includes loading of the cfg, but w/e)
    let startup_commands_start_time = std::time::Instant::now();

    if let Some(cfg) = options.config {
        load_cfg_as_global_cfg(&context, PathBuf::from(cfg));
    } else {
        load_global_cfg(&context);
    }

    // Store cmd duration in an env var
    context.scope.add_env_var(
        "CMD_DURATION_MS",
        startup_commands_start_time
            .elapsed()
            .as_millis()
            .to_string(),
    );

    if options.perf {
        eprintln!(
            "config loaded: {:?}",
            STOPWATCH
                .lock()
                .expect("unable to lock the stopwatch")
                .elapsed_split()
        );
        STOPWATCH
            .lock()
            .expect("unable to lock the stopwatch")
            .start();
    }

    //Configure rustyline
    let mut rl = default_rustyline_editor_configuration();
    let history_path = if let Some(cfg) = &context.configs().lock().global_config {
        let _ = configure_rustyline_editor(&mut rl, cfg);
        let helper = Some(nu_line_editor_helper(&context, cfg));
        rl.set_helper(helper);
        nu_data::config::path::history_path_or_default(cfg)
    } else {
        nu_data::config::path::default_history_path()
    };

    if options.perf {
        eprintln!(
            "rustyline configuration: {:?}",
            STOPWATCH
                .lock()
                .expect("unable to lock the stopwatch")
                .elapsed_split()
        );
        STOPWATCH
            .lock()
            .expect("unable to lock the stopwatch")
            .start();
    }

    // Don't load history if it's not necessary
    if options.save_history {
        let _ = rl.load_history(&history_path);
    }

    if options.perf {
        eprintln!(
            "history load: {:?}",
            STOPWATCH
                .lock()
                .expect("unable to lock the stopwatch")
                .elapsed_split()
        );
        STOPWATCH
            .lock()
            .expect("unable to lock the stopwatch")
            .start();
    }

    //set vars from cfg if present
    let (skip_welcome_message, prompt) = if let Some(cfg) = &context.configs().lock().global_config
    {
        (
            cfg.var("skip_welcome_message")
                .map(|x| x.is_true())
                .unwrap_or(false),
            cfg.var("prompt"),
        )
    } else {
        (false, None)
    };

    if options.perf {
        eprintln!(
            "load custom prompt: {:?}",
            STOPWATCH
                .lock()
                .expect("unable to lock the stopwatch")
                .elapsed_split()
        );
        STOPWATCH
            .lock()
            .expect("unable to lock the stopwatch")
            .start();
    }

    //Check whether dir we start in contains local cfg file and if so load it.
    load_local_cfg_if_present(&context);

    // Give ourselves a scope to work in
    context.scope.enter_scope();

    let mut session_text = String::new();
    let mut line_start: usize = 0;

    if !skip_welcome_message {
        println!(
            "Welcome to Nushell {} (type 'help' for more info)",
            nu_command::commands::core_version()
        );
    }

    #[cfg(windows)]
    {
        let _ = nu_ansi_term::enable_ansi_support();
    }

    let mut ctrlcbreak = false;

    if options.perf {
        eprintln!(
            "timing stopped. starting run loop: {:?}",
            STOPWATCH
                .lock()
                .expect("unable to lock the stopwatch")
                .elapsed_split()
        );
        STOPWATCH
            .lock()
            .expect("unable to lock the stopwatch")
            .stop();
    }

    loop {
        if context.ctrl_c().load(Ordering::SeqCst) {
            context.ctrl_c().store(false, Ordering::SeqCst);
            continue;
        }

        let cwd = context.shell_manager().path();

        // Check if the PROMPT_COMMAND env variable is set. This env variable
        // contains nu code that is used to overwrite the prompt
        let colored_prompt = match context.scope.get_env(PROMPT_COMMAND) {
            Some(env_prompt) => evaluate_prompt_string(&env_prompt, &context, &cwd),
            None => {
                if let Some(prompt) = &prompt {
                    let prompt_line = prompt.as_string()?;
                    evaluate_prompt_string(&prompt_line, &context, &cwd)
                } else {
                    default_prompt_string(&cwd)
                }
            }
        };

        let prompt = {
            if let Ok(bytes) = strip_ansi_escapes::strip(&colored_prompt) {
                String::from_utf8_lossy(&bytes).to_string()
            } else {
                "> ".to_string()
            }
        };

        if let Some(helper) = rl.helper_mut() {
            helper.colored_prompt = colored_prompt;
        }
        let mut initial_command = Some(String::new());
        let mut readline = Err(ReadlineError::Eof);
        while let Some(ref cmd) = initial_command {
            readline = rl.readline_with_initial(&prompt, (cmd, ""));
            initial_command = None;
        }

        if let Ok(line) = &readline {
            line_start = session_text.len();
            session_text.push_str(line);
            session_text.push('\n');
        }

        // start time for command duration
        let cmd_start_time = std::time::Instant::now();

        let line = match convert_rustyline_result_to_string(readline) {
            LineResult::Success(_) => process_script(
                &session_text[line_start..],
                &context,
                false,
                line_start,
                true,
            ),
            x => x,
        };

        // Store cmd duration in an env var
        context.scope.add_env_var(
            "CMD_DURATION_MS",
            cmd_start_time.elapsed().as_millis().to_string(),
        );

        match line {
            LineResult::Success(line) => {
                if options.save_history && !line.trim().is_empty() {
                    rl.add_history_entry(&line);
                    let _ = rl.append_history(&history_path);
                }
                maybe_print_errors(&context, Text::from(session_text.clone()));
            }

            LineResult::ClearHistory => {
                if options.save_history {
                    rl.clear_history();
                    std::fs::remove_file(&history_path)?;
                }
            }

            LineResult::Error(line, err) => {
                if options.save_history && !line.trim().is_empty() {
                    rl.add_history_entry(&line);
                    let _ = rl.append_history(&history_path);
                }

                context
                    .host()
                    .lock()
                    .print_err(err, &Text::from(session_text.clone()));

                // I am not so sure, we don't need maybe_print_errors here (as we printed an err
                // above), because maybe_print_errors also clears the errors.
                // TODO Analyze where above err comes from, and whether we need to clear
                // context.errors here
                // Or just be consistent and return errors always in context.errors...
                maybe_print_errors(&context, Text::from(session_text.clone()));
            }

            LineResult::CtrlC => {
                let config_ctrlc_exit = context
                    .configs()
                    .lock()
                    .global_config
                    .as_ref()
                    .and_then(|cfg| cfg.var("ctrlc_exit"))
                    .map(|ctrl_c| ctrl_c.is_true())
                    .unwrap_or(false); // default behavior is to allow CTRL-C spamming similar to other shells

                if !config_ctrlc_exit {
                    continue;
                }

                if ctrlcbreak {
                    if options.save_history {
                        let _ = rl.append_history(&history_path);
                    }
                    std::process::exit(0);
                } else {
                    context.with_host(|host| host.stdout("CTRL-C pressed (again to quit)"));
                    ctrlcbreak = true;
                    continue;
                }
            }

            LineResult::CtrlD => {
                context.shell_manager().remove_at_current();
                if context.shell_manager().is_empty() {
                    break;
                }
            }

            LineResult::Break => {
                break;
            }
        }
        ctrlcbreak = false;
    }

    // we are ok if we can not save history
    if options.save_history {
        let _ = rl.append_history(&history_path);
    }

    Ok(())
}

#[cfg(feature = "rustyline-support")]
pub fn load_local_cfg_if_present(context: &EvaluationContext) {
    trace!("Loading local cfg if present");
    match config::loadable_cfg_exists_in_dir(PathBuf::from(context.shell_manager().path())) {
        Ok(Some(cfg_path)) => {
            if let Err(err) = context.load_config(&ConfigPath::Local(cfg_path)) {
                context.host().lock().print_err(err, &Text::from(""))
            }
        }
        Err(e) => {
            //Report error while checking for local cfg file
            context.host().lock().print_err(e, &Text::from(""))
        }
        Ok(None) => {
            //No local cfg file present in start dir
        }
    }
}

fn load_cfg_as_global_cfg(context: &EvaluationContext, path: PathBuf) {
    if let Err(err) = context.load_config(&ConfigPath::Global(path)) {
        context.host().lock().print_err(err, &Text::from(""));
    }
}

pub fn load_global_cfg(context: &EvaluationContext) {
    match config::default_path() {
        Ok(path) => {
            load_cfg_as_global_cfg(context, path);
        }
        Err(e) => {
            context.host().lock().print_err(e, &Text::from(""));
        }
    }
}

pub fn register_plugins(context: &EvaluationContext) -> Result<(), ShellError> {
    if let Ok(plugins) = nu_engine::plugin::build_plugin::scan(search_paths()) {
        context.add_commands(
            plugins
                .into_iter()
                .filter(|p| !context.is_command_registered(p.name()))
                .collect(),
        );
    }

    Ok(())
}

pub fn parse_and_eval(line: &str, ctx: &EvaluationContext) -> Result<String, ShellError> {
    // FIXME: do we still need this?
    let line = if let Some(s) = line.strip_suffix('\n') {
        s
    } else {
        line
    };

    // TODO ensure the command whose examples we're testing is actually in the pipeline
    ctx.scope.enter_scope();
    let (classified_block, err) = nu_parser::parse(line, 0, &ctx.scope);
    if let Some(err) = err {
        ctx.scope.exit_scope();
        return Err(err.into());
    }

    let input_stream = InputStream::empty();

    let result = run_block(
        &classified_block,
        ctx,
        input_stream,
        ExternalRedirection::Stdout,
    );
    ctx.scope.exit_scope();

    result?.collect_string(Tag::unknown()).map(|x| x.item)
}

#[allow(dead_code)]
fn current_branch() -> String {
    #[cfg(feature = "shadow-rs")]
    {
        Some(shadow_rs::branch())
            .map(|x| x.trim().to_string())
            .filter(|x| !x.is_empty())
            .map(|x| format!("({})", x))
            .unwrap_or_default()
    }
    #[cfg(not(feature = "shadow-rs"))]
    {
        "".to_string()
    }
}
