Documentation
use console::style;
use inquire::Select;
use std::io::BufRead;
use std::path::{Path, PathBuf};
use std::process::{self, Command, Stdio};
use std::{env, io};

use crate::agents::Agent;
use crate::config::{get_default_agent, get_global_agent, DefaultAgent};
use crate::detect::{detect, AGENT_MAP};

#[derive(Clone)]
pub struct DetectOptions {
    pub cwd: PathBuf,
    pub auto_install: bool,
    pub programmatic: bool,
}
impl Default for DetectOptions {
    fn default() -> Self {
        DetectOptions {
            cwd: env::current_dir().unwrap(),
            auto_install: false,
            programmatic: false,
        }
    }
}
impl DetectOptions {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn with_auto_install(mut self, auto_install: bool) -> Self {
        self.auto_install = auto_install;
        self
    }
}

pub struct RunnerContext {
    pub programmatic: bool,
    pub has_lock: bool,
    pub cwd: PathBuf,
}

pub type Runner =
    fn(agent: Agent, args: Vec<String>, ctx: Option<RunnerContext>) -> (String, Vec<String>);

pub fn run_cli(func: Runner, options: Option<DetectOptions>) {
    let args = env::args().collect::<Vec<String>>()[1..]
        .to_vec()
        .into_iter()
        .filter(|v| !v.is_empty())
        .collect::<Vec<String>>();

    let mut options = match options {
        Some(o) => o,
        None => DetectOptions::default(),
    };

    run(func, args, &mut options)
}

pub fn run(func: Runner, args: Vec<String>, options: &mut DetectOptions) {
    let version = env!("CARGO_PKG_VERSION");

    let mut args = args;
    if args.len() > 2 && args[0] == "-C" {
        let path = Path::new(args[1].as_str());
        options.cwd = if path.is_absolute() {
            path.to_path_buf()
        } else {
            options.cwd.join(path)
        };
        args = args[0..2].to_vec();
    }

    if args.len() == 1 && (args[0].to_lowercase() == "-v" || args[0] == "--version") {
        println!("nci   {}", style(format!("v{}", version)).blue());
        return;
    }
    if args.len() == 1 && (args[0] == "-h" || args[0] == "--help") {
        println!("nci use the right package manager v{}\n", version);
        println!("ni    -   install");
        println!("nr    -   run");
        println!("nlx   -   execute");
        println!("nu    -   upgrade");
        println!("nun   -   uninstall");
        println!("nci   -   clean install");
        println!("na    -   agent alias");
        print!("ni -v -   show used agent");
        println!(
            "{}",
            style("\ncheck https://github.com/Debbl/nci for more documentation.").blue()
        );
        return;
    }

    let command = get_cli_command(func, args.clone(), options.clone());

    if let Some((agent, args)) = command {
        println!(
            "{} {}",
            style("Running:").dim(),
            style(format!("{} {}", agent, args.join(" "))).green()
        );

        execa_command(&agent, Some(args)).unwrap();
    } else {
        return;
    }
}

fn get_cli_command(
    func: Runner,
    args: Vec<String>,
    options: DetectOptions,
) -> Option<(String, Vec<String>)> {
    let global = "-g".to_string();
    if args.contains(&global) {
        return Some(func(get_global_agent(), args, None));
    }
    let mut _agent = DefaultAgent::Prompt;
    if let Some(v) = detect(options.clone()) {
        _agent = DefaultAgent::Agent(v);
    } else {
        _agent = get_default_agent(options.clone().programmatic);
    }

    if _agent == DefaultAgent::Prompt {
        let items: Vec<&&str> = AGENT_MAP.keys().filter(|x| !x.contains("@")).collect();
        let selection = Select::new("script to run:", items).prompt();
        if let Ok(selection) = selection {
            let value = AGENT_MAP.get(selection);
            if let Some(value) = value {
                _agent = DefaultAgent::Agent(value.clone());
            } else {
                return None;
            }
        } else {
            process::exit(1)
        }
    }
    let runner_ctx = RunnerContext {
        programmatic: options.programmatic,
        has_lock: true,
        cwd: options.cwd,
    };
    match _agent {
        DefaultAgent::Agent(agent) => Some(func(agent, args, Some(runner_ctx))),
        DefaultAgent::Prompt => Some(func(Agent::Npm, args, Some(runner_ctx))),
    }
}

pub fn execa_command(agent: &str, args: Option<Vec<String>>) -> Result<(), io::Error> {
    let mut command = Command::new(agent)
        .args(args.unwrap_or_default())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .spawn()
        .expect("Failed to execute command");

    if let Some(stdout) = command.stdout.take() {
        let reader = io::BufReader::new(stdout);
        for line in reader.lines() {
            if let Ok(line) = line {
                println!("{}", line);
            }
        }
    }

    command.wait()?;

    Ok(())
}