use std::collections::HashMap;
use std::io::{PipeReader, PipeWriter};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use crate::parse::{Ast, PostExpansion, PreExpansion};
use crate::*;

mod builtin;

#[derive(Debug)]
enum ExecError {
    UnknownVariable(BString),
    ExecError(i32),
}

#[derive(Clone)]
struct Executor {
    se: Arc<Mutex<Session>>,
    args: Option<Vec<BString>>,
}

impl Executor {
    fn execute_pipeline(
        &mut self,
        pipes: parse::Pipes<parse::PostExpansion>,
        mut stdin: Option<PipeReader>,
        mut stdout: Option<PipeWriter>,
    ) -> Result<(), ExecError> {
        let mut children = Vec::new();
        let mut threads = Vec::new();
        let mut prev_reader = None;
        let mut spawn_error = false;

        let pipelen = pipes.cmds.len();
        for (i, cmd) in pipes.cmds.into_iter().enumerate() {
            let (reader, writer) = if i < pipelen - 1 {
                let (r, w) = io::pipe().unwrap();
                (Some(r), Some(w))
            } else {
                (None, None)
            };

            let dc = get_command_kind(&self.se.lock().unwrap(), &cmd.cmd[..]);

            match dc {
                CommandKind::Path(path) => {
                    let mut command = Command::new(&path);
                    for arg in cmd.args.iter() {
                        command.arg(OsStr::from_bytes(arg));
                    }

                    if let Some(r) = prev_reader.take() {
                        command.stdin(Stdio::from(r));
                    } else if let Some(stdin) = stdin.take() {
                        command.stdin(Stdio::from(stdin));
                    }

                    if let Some(w) = writer {
                        command.stdout(Stdio::from(w));
                    } else if let Some(stdout) = stdout.take() {
                        command.stdout(Stdio::from(stdout));
                    }

                    let child = match command.spawn() {
                        Ok(c) => c,
                        Err(e) => {
                            let cmd = path.to_string_lossy();
                            let msg = match e.kind() {
                                io::ErrorKind::NotFound => format!("{cmd} not found"),
                                io::ErrorKind::PermissionDenied => {
                                    format!("{cmd} is not executable")
                                }
                                io::ErrorKind::FileTooLarge => format!("{cmd} is too massive"),
                                io::ErrorKind::ResourceBusy | io::ErrorKind::ExecutableFileBusy => {
                                    format!("{cmd} is busy")
                                }
                                io::ErrorKind::TooManyLinks => {
                                    format!("{cmd} could not be resolved")
                                }
                                io::ErrorKind::InvalidFilename => {
                                    format!("{cmd} is not a valid file name")
                                }
                                io::ErrorKind::ArgumentListTooLong => format!("too many arguments"),
                                io::ErrorKind::Interrupted => format!("got interrupted"),
                                io::ErrorKind::Unsupported => format!("{cmd} is not supported"),
                                e => format!("I am surprised you can get this error here: {e:?}"),
                            };
                            println!("pish: {msg}");
                            spawn_error = true;
                            break;
                        }
                    };

                    children.push(child);
                }

                CommandKind::Builtin(builtin) => {
                    let mut input: Box<dyn io::Read + Send> = if let Some(r) = prev_reader.take() {
                        Box::new(r)
                    } else if let Some(stdin) = stdin.take() {
                        Box::new(stdin)
                    } else {
                        Box::new(io::stdin())
                    };

                    let mut output: Box<dyn io::Write + Send> = if let Some(w) = writer {
                        Box::new(w)
                    } else if let Some(w) = stdout.take() {
                        Box::new(w)
                    } else {
                        Box::new(io::stdout())
                    };

                    // currently only required for `re`, cannot happen in background thread
                    builtin.special(self.se.clone(), &cmd.args);

                    let cloned_session = self.se.clone();
                    let handle = std::thread::spawn(move || {
                        builtin.io(cloned_session, &cmd.args, &mut input, &mut output)
                    });

                    threads.push(handle);
                }

                CommandKind::Fun(ast) => {
                    let mut this = self.clone();

                    let mut args = Vec::with_capacity(cmd.args.len() + 1);
                    args.push(cmd.cmd.clone());
                    args.extend_from_slice(&cmd.args);
                    this.args = Some(args);

                    let ast = ast.expand(&mut this)?;
                    this.execute(ast, prev_reader, writer).unwrap();
                }
            }

            prev_reader = reader;
        }

        if spawn_error {
            for child in children.iter_mut() {
                if let Err(e) = child.kill() {
                    println!("failed to kill child - {e:?}");
                }
            }
            return Err(ExecError::ExecError(127));
        }

        let mut code = 0;
        for jh in threads {
            match jh.join() {
                Ok(Ok(())) => (),
                Ok(Err(e)) => match e {
                    BuiltinError::IO(_) => code = -1,
                    BuiltinError::Exit(c) => code = c,
                },
                Err(_) => code = 127,
            }
        }
        for child in children.iter_mut() {
            match child.wait() {
                Ok(ec) => {
                    if let Some(c) = ec.code() {
                        code = c;
                    }
                }
                Err(e) => {
                    println!("failed to wait for child - {e:?}")
                }
            }
        }

        if code == 0 {
            Ok(())
        } else {
            Err(ExecError::ExecError(code))
        }
    }

    fn execute_var_assign(&mut self, va: parse::VarAssign<PostExpansion>) -> Result<(), ExecError> {
        self.se.lock().unwrap().vars.insert(va.var, va.val);
        Ok(())
    }

    fn execute_fun_decl(&mut self, fd: parse::FunDecl<PostExpansion>) -> Result<(), ExecError> {
        self.se.lock().unwrap().funs.insert(fd.name, *fd.body.body);
        Ok(())
    }

    fn execute(
        &mut self,
        ast: Ast<parse::PostExpansion>,
        stdin: Option<PipeReader>,
        stdout: Option<PipeWriter>,
    ) -> Result<(), ExecError> {
        match ast {
            Ast::FunDecl(fd) => self.execute_fun_decl(fd),
            Ast::VarAssign(va) => self.execute_var_assign(va),
            Ast::Pipes(pipes) => self.execute_pipeline(pipes, stdin, stdout),
        }
    }
}

impl parse::Expander for Executor {
    type Error = ExecError;

    fn expand_var(&mut self, var: BString) -> Result<BString, Self::Error> {
        if var.is_empty() {
            return Err(ExecError::UnknownVariable(var));
        }

        if var[0].is_ascii_digit() {
            if let Some(x) = String::from_utf8(var.clone())
                .ok()
                .and_then(|x| x.parse::<usize>().ok())
            {
                if let Some(args) = &self.args {
                    if x < args.len() {
                        return Ok(args[x].clone());
                    }
                } else if let Some(arg) = std::env::args_os().skip(x).next() {
                    return Ok(arg.into_encoded_bytes());
                }
            }
        }

        if let Some(val) = self.se.lock().unwrap().vars.get(&var) {
            return Ok(val.clone());
        }

        match std::env::var_os(OsStr::from_bytes(&var)) {
            Some(val) => Ok(val.as_bytes().to_vec()),
            None => Err(ExecError::UnknownVariable(var)),
        }
    }

    fn expand_cmd(&mut self, ast: Ast<parse::PostExpansion>) -> Result<BString, Self::Error> {
        let (stdin, _) = io::pipe().unwrap();
        let (mut expansion, stdout) = io::pipe().unwrap();
        let mut this = self.clone();
        let t = std::thread::spawn(move || this.execute(ast, Some(stdin), Some(stdout)));
        let mut buf = Vec::new();
        expansion.read_to_end(&mut buf).unwrap();
        t.join().unwrap()?;
        if buf.last() == Some(&b'\n') {
            buf.pop();
        }
        Ok(buf)
    }
}

fn exec(se: Arc<Mutex<Session>>, ast: Ast<PreExpansion>) -> Result<(), ExecError> {
    let mut exec = Executor { se, args: None };
    let ast = ast.expand(&mut exec)?;
    exec.execute(ast, None, None)
}

pub fn run(se: Arc<Mutex<Session>>, cmd: Vec<u8>) {
    let parsed = parse::do_parse(&cmd);
    let parsed = match parsed {
        Ok(p) => p,
        Err(err) => {
            se.lock().unwrap().raw.disable();
            println!("{:?}: {}", err.0, String::from_utf8_lossy(&err.1));
            print!("{}", se.lock().unwrap().prompt());
            std::io::stdout().lock().flush().unwrap();
            se.lock().unwrap().raw.enable();
            return;
        }
    };

    se.lock().unwrap().raw.disable();
    let result = exec(se.clone(), parsed);
    se.lock().unwrap().raw.enable();

    let status_string = match result {
        Ok(_) => String::new(),
        Err(ExecError::UnknownVariable(var)) => {
            format!("unbound variable: {}", String::from_utf8_lossy(&var))
        }
        Err(ExecError::ExecError(i)) => i.to_string(),
    };

    print!("\r{status_string}\r\n{}", se.lock().unwrap().prompt());
    let _ = std::io::stdout().lock().flush();
}

#[derive(Debug)]
pub enum BuiltinError {
    IO(std::io::Error),
    Exit(i32),
}

impl From<std::io::Error> for BuiltinError {
    fn from(value: std::io::Error) -> Self {
        Self::IO(value)
    }
}

type BuiltinResult = Result<(), BuiltinError>;

#[allow(unused_variables)]
pub trait Builtin: Send + Sync {
    fn name(&self) -> &str;

    fn special(&self, session: Arc<Mutex<Session>>, args: &[BString]) {}

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> BuiltinResult;
}

const BUILTINS: &[&'static dyn Builtin] = &[
    &builtin::cd,
    &builtin::clear,
    #[cfg(debug_assertions)]
    &builtin::re,
    &builtin::sink("to", false),
    &builtin::sink("into", false),
    &builtin::sink("append", true),
    &builtin::from,
    &builtin::builtins,
    &builtin::_type,
    &builtin::history,
    &builtin::escape,
    &builtin::parse,
    &builtin::null,
];

pub fn builtin_map() -> HashMap<BString, &'static dyn Builtin> {
    let mut map = HashMap::new();
    for &b in BUILTINS {
        map.insert(b.name().as_bytes().to_vec(), b);
    }
    map
}

#[derive(Clone)]
pub enum CommandKind {
    Builtin(&'static dyn Builtin),
    Fun(Ast<PreExpansion>),
    Path(PathBuf),
}

pub fn get_command_kind(se: &Session, cmd: &bstr) -> CommandKind {
    let path_cmd = CommandKind::Path(PathBuf::from(OsStr::from_bytes(cmd)));
    if cmd.contains(&b'/') {
        path_cmd
    } else if let Some(fun) = se.funs.get(cmd) {
        CommandKind::Fun(fun.clone())
    } else if let Some(builtin) = se.builtins.get(cmd) {
        CommandKind::Builtin(*builtin)
    } else {
        path_cmd
    }
}
