use std::collections::HashMap;
use std::fs::File;
use std::io::{PipeReader, PipeWriter};
use std::path::PathBuf;
use std::process::Child;
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;

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

mod builtin;

#[derive(Debug)]
pub enum ExecError {
    UnknownVariable(BString),
    ExecError(i32),
    SpawnIO(String, std::io::Error),
    IO(std::io::Error),
    ErrorStack(Vec<ExecError>),
    Panic,
}

impl ExecError {
    pub fn error_message(&self) -> String {
        match self {
            ExecError::UnknownVariable(items) => {
                format!("unknown variable: {}", String::from_utf8_lossy(items))
            }
            ExecError::ExecError(exit_code) => format!("{exit_code}"),
            ExecError::SpawnIO(cmd, error) => match error.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 => String::from("too many arguments"),
                io::ErrorKind::Interrupted => String::from("got interrupted"),
                io::ErrorKind::Unsupported => format!("{cmd} is not supported"),
                e => format!("I am surprised you can get this error here: {e:?}"),
            },
            ExecError::IO(error) => format!("{error:?}"),
            ExecError::Panic => String::from("worker thread panicked"),
            ExecError::ErrorStack(stack) => {
                let mut out = String::new();
                for e in stack.iter() {
                    if !out.is_empty() {
                        out += "\n";
                    }
                    out += &e.error_message();
                }
                out
            }
        }
    }
}

impl From<BuiltinError> for ExecError {
    fn from(value: BuiltinError) -> Self {
        match value {
            BuiltinError::IO(error) => Self::IO(error),
            BuiltinError::Exit(code) => Self::ExecError(code),
        }
    }
}

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

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

pub enum Input {
    Stdin,
    Pipe(PipeReader),
    File(File),
}

pub enum Output {
    Stdout,
    Pipe(PipeWriter),
    File(File),
}

impl From<Input> for Stdio {
    fn from(value: Input) -> Self {
        match value {
            Input::Stdin => Stdio::inherit(),
            Input::Pipe(reader) => reader.into(),
            Input::File(file) => file.into(),
        }
    }
}

impl From<Output> for Stdio {
    fn from(value: Output) -> Stdio {
        match value {
            Output::Stdout => Stdio::inherit(),
            Output::Pipe(writer) => writer.into(),
            Output::File(file) => file.into(),
        }
    }
}

impl Read for Input {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        match self {
            Input::Stdin => io::stdin().read(buf),
            Input::Pipe(reader) => reader.read(buf),
            Input::File(file) => file.read(buf),
        }
    }
}

impl Write for Output {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        match self {
            Output::Stdout => io::stdout().write(buf),
            Output::Pipe(writer) => writer.write(buf),
            Output::File(file) => file.write(buf),
        }
    }

    fn flush(&mut self) -> io::Result<()> {
        match self {
            Output::Stdout => io::stdout().flush(),
            Output::Pipe(writer) => writer.flush(),
            Output::File(file) => file.flush(),
        }
    }
}

enum SpawnedCmd {
    Builtin(JoinHandle<Result<(), BuiltinError>>),
    Fun(JoinHandle<Result<(), ExecError>>),
    Child(Child),
    SpawnError(io::Error),
}

impl SpawnedCmd {
    fn join(self) -> Result<(), ExecError> {
        match self {
            SpawnedCmd::Builtin(handle) => handle.join().map_err(|_| ExecError::Panic)??,
            SpawnedCmd::Fun(handle) => handle.join().map_err(|_| ExecError::Panic)??,
            SpawnedCmd::Child(mut child) => {
                let exit_code = child.wait()?;
                match exit_code.code() {
                    Some(0) => (),
                    Some(x) => Err(ExecError::ExecError(x))?,
                    None => Err(ExecError::ExecError(-1))?,
                }
            }
            SpawnedCmd::SpawnError(err) => Err(err)?,
        }
        Ok(())
    }
}

impl Executor {
    pub fn new_for_completion(se: Arc<Mutex<Session>>) -> Self {
        Self {
            se,
            args: None,
            expand_commands: false,
        }
    }

    fn spawn_cmd(
        &mut self,
        cmd: CommandKind,
        args: Vec<BString>,
        mut stdin: Input,
        mut stdout: Output,
    ) -> SpawnedCmd {
        match cmd {
            CommandKind::Builtin(builtin) => {
                builtin.special(self.se.clone(), &args[1..]);
                let cloned_session = self.se.clone();
                let handle = std::thread::spawn(move || {
                    builtin.io(cloned_session, &args[1..], &mut stdin, &mut stdout)
                });
                SpawnedCmd::Builtin(handle)
            }
            CommandKind::Fun(ast) => {
                let mut this = self.clone();
                this.args = Some(args);

                let handle = std::thread::spawn(move || {
                    let ast = ast.expand(&mut this)?;
                    this.execute(ast, stdin, stdout)?;
                    Ok(())
                });

                SpawnedCmd::Fun(handle)
            }
            CommandKind::Path(path) => {
                let mut command = Command::new(&path);
                for arg in args.iter().skip(1) {
                    command.arg(OsStr::from_bytes(arg));
                }

                command.stdin(stdin);
                command.stdout(stdout);

                crate::export_fun::prepare_command(self.se.clone(), &mut command);

                match command.spawn() {
                    Ok(c) => SpawnedCmd::Child(c),
                    Err(e) => SpawnedCmd::SpawnError(e),
                }
            }
        }
    }

    fn execute_pipeline(
        &mut self,
        pipes: parse::Pipes<parse::PostExpansion>,
        stdin: Input,
        stdout: Output,
    ) -> Result<(), ExecError> {
        let mut inputs = Vec::with_capacity(pipes.cmds.len());
        let mut outputs = Vec::with_capacity(pipes.cmds.len());
        let mut executors = Vec::with_capacity(pipes.cmds.len());

        inputs.push(stdin);
        for _ in 1..pipes.cmds.len() {
            let (r, w) = io::pipe().unwrap();
            inputs.push(Input::Pipe(r));
            outputs.push(Output::Pipe(w));
        }
        outputs.push(stdout);

        assert_eq!(pipes.cmds.len(), inputs.len());
        assert_eq!(pipes.cmds.len(), outputs.len());

        for (mut cmd, (stdin, stdout)) in pipes
            .cmds
            .into_iter()
            .zip(inputs.into_iter().zip(outputs.into_iter()))
        {
            let dc = get_command_kind(&self.se.lock().unwrap(), &cmd.cmd[..]);
            cmd.args.insert(0, cmd.cmd.clone());
            executors.push(self.spawn_cmd(dc, cmd.args, stdin, stdout));
        }

        let mut code = None;
        for e in executors {
            if let Some(err) = e.join().err() {
                code = Some(err);
            }
        }

        if let Some(err) = code {
            Err(err)
        } else {
            Ok(())
        }
    }

    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.clone(), *fd.body.body);
        crate::export_fun::create_function_hook(self.se.clone(), &fd.name);
        // TODO: very ugly to ad-hoc keep export stuff & session data in sync here
        Ok(())
    }

    fn execute(
        &mut self,
        ast: Ast<parse::PostExpansion>,
        stdin: Input,
        stdout: Output,
    ) -> 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),
        }
    }

    pub fn execute_fun(
        session: Arc<Mutex<Session>>,
        invoke: Vec<BString>,
        stdin: Input,
        stdout: Output,
    ) -> Result<(), ExecError> {
        let mut this = Self {
            se: session,
            args: None,
            expand_commands: true,
        };

        let cmd = parse::Command {
            cmd: invoke[0].clone(),
            args: invoke[1..].to_vec(),
        };

        this.execute_pipeline(parse::Pipes { cmds: vec![cmd] }, stdin, stdout)
    }
}

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

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

        if var[0].is_ascii_digit()
            && 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().nth(x) {
                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 => match default {
                Some(d) => Ok(d),
                None => Err(ExecError::UnknownVariable(var)),
            },
        }
    }

    fn expand_cmd(&mut self, ast: Ast<parse::PostExpansion>) -> Result<BString, Self::Error> {
        if !self.expand_commands {
            return Err(ExecError::ExecError(-1));
        }

        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, Input::Pipe(stdin), Output::Pipe(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,
        expand_commands: true,
    };
    let ast = ast.expand(&mut exec)?;
    exec.execute(ast, Input::Stdin, Output::Stdout)
}

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(e) => format!("{}\r\n", e.error_message()),
    };

    print!("\r{status_string}{}", 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
    }
}
