use crate::rw::*;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;

use crate::parse::{self, Ast, PostExpansion, PreExpansion};
use crate::wait::{ChildWaiter, ThreadWaiter};
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,
    AliasDepthExceeded,
    Parse(crate::parse::ParseError),
    Break,
    Continue,
}

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 += "\r\n";
                    }
                    out += &e.error_message();
                }
                out
            }
            ExecError::AliasDepthExceeded => "alias depth exceeded".to_string(),
            ExecError::Parse(pe) => format!("parse error: {pe:?}"),
            ExecError::Break => "break: only useful in loops".to_string(),
            ExecError::Continue => "continue: only useful in loops".to_string(),
        }
    }
}

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

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 struct SpawnedPipeline {
    executors: Vec<SpawnedCmd>,
    cancel: Vec<Canceler>,
}

pub enum SpawnedCmd {
    Builtin(ThreadWaiter<Result<(), BuiltinError>>),
    Fun(ThreadWaiter<Result<(), ExecError>>),
    Child(ChildWaiter),
    SpawnError(io::Error),
    Joined(Result<(), ExecError>),
    Pipeline(SpawnedPipeline),
}

trait ExecResultAsBool {
    fn as_bool(&self) -> Result<bool, ExecError>;
}
impl ExecResultAsBool for Result<(), ExecError> {
    /// returns the boolean value interpreted from this exit code.
    ///
    /// in case the exit code indicates divergence (break/continue) that "error" will be propagated
    fn as_bool(&self) -> Result<bool, ExecError> {
        match self {
            Ok(_) | Err(ExecError::ExecError(0)) => Ok(true),
            Err(ExecError::Break) => Err(ExecError::Break),
            Err(ExecError::Continue) => Err(ExecError::Continue),
            Err(_) => Ok(false),
        }
    }
}

impl SpawnedCmd {
    pub fn join(self) -> Result<(), ExecError> {
        match self {
            SpawnedCmd::Builtin(handle) => {
                handle.into_inner().join().map_err(|_| ExecError::Panic)??
            }
            SpawnedCmd::Fun(handle) => {
                handle.into_inner().join().map_err(|_| ExecError::Panic)??
            }
            SpawnedCmd::Child(child) => {
                let mut child = child.into_inner();
                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)?,
            SpawnedCmd::Joined(res) => res?,
            SpawnedCmd::Pipeline(pipes) => {
                let joined: Vec<_> = pipes
                    .executors
                    .into_iter()
                    .filter_map(|e| e.join().err())
                    .collect();
                match joined.len() {
                    0 => (),
                    1 => return Err(joined.into_iter().next().unwrap()),
                    _ => return Err(ExecError::ErrorStack(joined)),
                }
            }
        }
        Ok(())
    }

    /// returns whether the spawned command is already joined
    pub fn join_timeout(&mut self, timeout_ms: u16) -> bool {
        match self {
            SpawnedCmd::Builtin(tw) => tw.try_join(timeout_ms),
            SpawnedCmd::Fun(tw) => tw.try_join(timeout_ms),
            SpawnedCmd::Child(child) => !matches!(child.wait(timeout_ms), Ok(None)),
            SpawnedCmd::SpawnError(_) => true,
            SpawnedCmd::Joined(_) => true,
            SpawnedCmd::Pipeline(pipes) => pipes
                .executors
                .iter_mut()
                .all(|e| e.join_timeout(timeout_ms)),
        }
    }

    pub fn cancel(&mut self) {
        match self {
            SpawnedCmd::Pipeline(pipes) => {
                for c in pipes.cancel.iter_mut() {
                    c.cancel();
                }
            }
            SpawnedCmd::Builtin(_)
            | SpawnedCmd::Fun(_)
            | SpawnedCmd::Child(_)
            | SpawnedCmd::SpawnError(_)
            | SpawnedCmd::Joined(_) => (),
        }
    }
}

impl Executor {
    fn exec_loop(&mut self, mut s: SpawnedCmd, cs: &mut [Canceler]) -> Result<(), ExecError> {
        let begin = Instant::now();
        while !s.join_timeout(50) {
            if let Ok(se) = self.se.try_lock()
                && ctrlc::pressed_since(&se, begin)
            {
                for c in cs.iter_mut() {
                    c.cancel();
                }
                s.cancel();
                break;
            }
        }
        s.join()
    }

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

    pub fn new(se: Arc<Mutex<Session>>) -> Self {
        Self {
            se,
            args: None,
            expand_commands: true,
        }
    }

    fn spawn_cmd(
        &mut self,
        cmd: CommandKind,
        args: Vec<BString>,
        mut stdin: InputReader,
        mut stdout: OutputWriter,
    ) -> SpawnedCmd {
        match cmd {
            CommandKind::Builtin(builtin) => {
                let mut builtin = builtin.clone_box();
                builtin.special(self.se.clone(), &args[1..]);
                let cloned_session = self.se.clone();
                let handle = wait::spawn(move || {
                    builtin.io(cloned_session, &args[1..], &mut stdin, &mut stdout)
                });
                SpawnedCmd::Builtin(handle)
            }
            CommandKind::Fun(body) => {
                let mut this = self.clone();
                this.args = Some(args);
                this.execute_block(body, stdin, stdout)
            }
            CommandKind::Path(path) => {
                let mut command = Command::new(&path);
                for arg in args.iter().skip(1) {
                    command.arg(OsStr::from_bytes(arg));
                }

                command.stdin(Input::from(stdin));
                command.stdout(Output::from(stdout));

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

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

    fn execute_block(
        &mut self,
        block: parse::Block,
        stdin: InputReader,
        stdout: OutputWriter,
    ) -> SpawnedCmd {
        let mut this = self.clone();
        let handle = wait::spawn(move || -> Result<(), ExecError> {
            this.execute_block_inner(&block, stdin, stdout)
        });
        SpawnedCmd::Fun(handle)
    }

    fn execute_pipeline(
        &mut self,
        pipes: parse::Pipes<parse::PostExpansion>,
        stdin: InputReader,
        stdout: OutputWriter,
    ) -> SpawnedCmd {
        let mut inputs = Vec::with_capacity(pipes.cmds.len());
        let mut outputs = Vec::with_capacity(pipes.cmds.len());
        let mut cancel = Vec::with_capacity(pipes.cmds.len() * 2);
        let mut executors = Vec::with_capacity(pipes.cmds.len());

        inputs.push(stdin);
        for _ in 1..pipes.cmds.len() {
            let (r, w) = io::pipe().unwrap();
            let (i, c1) = InputReader::new(Input::Pipe(r));
            let (o, c2) = OutputWriter::new(Output::Pipe(w));
            inputs.push(i);
            outputs.push(o);
            cancel.push(c1);
            cancel.push(c2);
        }
        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));
        }

        SpawnedCmd::Pipeline(SpawnedPipeline { executors, cancel })
    }

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

    fn execute_fun_decl(&mut self, fd: parse::FunDecl<PostExpansion>) -> SpawnedCmd {
        self.se
            .lock()
            .unwrap()
            .funs
            .insert(fd.name.clone(), fd.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
        SpawnedCmd::Joined(Ok(()))
    }

    fn execute(
        &mut self,
        ast: Ast<parse::PostExpansion>,
        stdin: InputReader,
        stdout: OutputWriter,
    ) -> SpawnedCmd {
        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),
            Ast::If(cond) => self.execute_if(cond, stdin, stdout),
            Ast::While(w) => self.execute_while(w, 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(),
        };

        let (stdin, c1) = InputReader::new(stdin);
        let (stdout, c2) = OutputWriter::new(stdout);

        let cmd = this.execute_pipeline(parse::Pipes { cmds: vec![cmd] }, stdin, stdout);
        this.exec_loop(cmd, &mut [c1, c2])
    }

    fn execute_block_inner(
        &mut self,
        block: &Block,
        stdin: InputReader,
        stdout: OutputWriter,
    ) -> Result<(), ExecError> {
        for cmd in block.commands.iter() {
            let cmd = cmd.clone().expand(self)?;
            self.execute(cmd, stdin.try_clone()?, stdout.try_clone()?)
                .join()?;
        }
        Ok(())
    }

    fn execute_if(
        &mut self,
        cond: parse::If<PostExpansion>,
        stdin: InputReader,
        stdout: OutputWriter,
    ) -> SpawnedCmd {
        let mut this = self.clone();
        let handle = wait::spawn(move || -> Result<(), ExecError> {
            let res = this
                .execute_pipeline(cond.condition, stdin.try_clone()?, stdout.try_clone()?)
                .join();
            let block = if res.as_bool()? {
                cond.true_block
            } else {
                cond.false_block
            };
            this.execute_block_inner(&block, stdin.try_clone()?, stdout.try_clone()?)
        });
        SpawnedCmd::Fun(handle)
    }

    fn execute_while(
        &mut self,
        w: parse::While,
        stdin: InputReader,
        stdout: OutputWriter,
    ) -> SpawnedCmd {
        let mut this = self.clone();
        let parse::While { condition, block } = w;
        let handle = wait::spawn(move || -> Result<(), ExecError> {
            while {
                let condition = condition.clone().expand(&mut this)?;
                this.execute_pipeline(condition, stdin.try_clone()?, stdout.try_clone()?)
                    .join()
                    .as_bool()?
            } {
                let res = this.execute_block_inner(&block, stdin.try_clone()?, stdout.try_clone()?);
                match res {
                    Ok(_) => (),
                    Err(ExecError::Break) => break,
                    Err(ExecError::Continue) => continue,
                    Err(e) => Err(e)?,
                }
            }
            Ok(())
        });
        SpawnedCmd::Fun(handle)
    }

    pub fn execute_script(
        &mut self,
        s: parse::Script,
        stdin: InputReader,
        stdout: OutputWriter,
    ) -> SpawnedCmd {
        self.execute_block(parse::Block { commands: s.stmts }, stdin, stdout)
    }
}

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

    fn expand_var(
        &mut self,
        var: BString,
        default: Option<BString>,
    ) -> Result<BString, Self::Error> {
        match &var[..] {
            b"CWD_PRETTY" => return Ok(crate::pretty_cwd().into_bytes()),
            b"PISH_VERSION" => return Ok(crate::consts::PISH_VERSION.as_bytes().to_vec()),
            b"PISH_COMMIT" => return Ok(crate::consts::PISH_COMMIT.as_bytes().to_vec()),
            b"PISH_DIRTY" => return Ok(vec![crate::consts::PISH_DIRTY as u8 + b'0']),
            _ => {}
        }

        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 (stdin, c1) = InputReader::new(Input::Pipe(stdin));
        let (mut expansion, stdout) = io::pipe().unwrap();
        let (stdout, c2) = OutputWriter::new(Output::Pipe(stdout));
        let mut this = self.clone();

        let t = std::thread::spawn(move || {
            let cmd = this.execute(ast, stdin, stdout);
            this.exec_loop(cmd, &mut [c1, c2])
        });
        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)
    }

    type AliasAge = AliasAge;

    fn expand_alias(
        &mut self,
        cmd: &bstr,
        older_than: Option<AliasAge>,
    ) -> Result<Option<(AliasAge, Vec<ExpString>)>, Self::Error> {
        Ok(self
            .se
            .lock()
            .unwrap()
            .aliases
            .get(cmd, older_than.unwrap_or(AliasAge::MAX)))
    }
}

type AliasAge = u32;
type AliasBody = Vec<ExpString>;
type AliasSet = Vec<(AliasAge, AliasBody)>;

pub struct Aliases {
    age: u32,
    aliases: HashMap<BString, AliasSet>,
}

impl Aliases {
    pub fn new() -> Self {
        Self {
            age: 0,
            aliases: HashMap::new(),
        }
    }

    fn insert(&mut self, name: BString, body: AliasBody) {
        let age = self.age;
        self.age += 1;
        let new = (age, body);
        if let Some(entry) = self.aliases.get_mut(&name) {
            entry.push(new);
        } else {
            self.aliases.insert(name, vec![new]);
        }
    }

    fn get(&self, name: &bstr, older_than: AliasAge) -> Option<(AliasAge, AliasBody)> {
        let Some(alias_set) = self.aliases.get(name) else {
            return None;
        };

        alias_set.iter().rev().find(|e| e.0 < older_than).cloned()
    }

    fn remove(&mut self, name: &bstr) {
        self.aliases.remove(name);
    }
}

fn exec(se: Arc<Mutex<Session>>, ast: Ast<PreExpansion>) -> Result<(), ExecError> {
    let mut exec = Executor::new(se.clone());
    let ast = ast.expand(&mut exec)?;
    let (stdin, c1) = InputReader::new(Input::Stdin);
    let (stdout, c2) = OutputWriter::new(Output::Stdout);
    let cmd = exec.execute(ast, stdin, stdout);
    exec.exec_loop(cmd, &mut [c1, c2])
}

pub fn run_quiet(
    se: Arc<Mutex<Session>>,
    cmd: parse::Command<PostExpansion>,
) -> Result<(), ExecError> {
    let mut exec = Executor {
        se: se.clone(),
        args: None,
        expand_commands: true,
    };
    let (i, c1) = InputReader::new(Input::Null);
    let (o, c2) = OutputWriter::new(Output::Null);
    let cmd = exec.execute_pipeline(parse::Pipes { cmds: vec![cmd] }, i, o);
    exec.exec_loop(cmd, &mut [c1, c2])
}

pub fn run(se: Arc<Mutex<Session>>, parsed: Ast<PreExpansion>) {
    se.lock().unwrap().raw_disable();
    let result = exec(se.clone(), parsed);
    se.lock().unwrap().raw_enable();

    if se.lock().unwrap().loud {
        let status_string = match result {
            Ok(_) => String::new(),
            Err(e) => format!("{}\r\n", e.error_message()),
        };

        print!("\r{status_string}");
        Session::reprint_prompt(se);
        let _ = std::io::stdout().lock().flush();
    }
}

pub fn run_script(se: Arc<Mutex<Session>>, script: crate::parse::Script) {
    for stmt in script.stmts {
        run(se.clone(), stmt);
    }
}

pub fn source(se: Arc<Mutex<Session>>, file: &bstr) -> Result<(), ExecError> {
    let mut f = File::open(OsStr::from_bytes(file)).map_err(ExecError::IO)?;
    let mut buf = Vec::new();
    f.read_to_end(&mut buf).map_err(ExecError::IO)?;
    let script = parse::Script::parse_from_bytes(&buf).map_err(ExecError::Parse)?;
    // TODO: error propagation
    run_script(se, script);
    Ok(())
}

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

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(&mut 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;
}

pub trait BuiltinClone: Builtin {
    fn clone_box(&self) -> Box<dyn Builtin>;
}

impl<T: 'static + Builtin + Clone> BuiltinClone for T {
    fn clone_box(&self) -> Box<dyn Builtin> {
        Box::new(self.clone())
    }
}

const BUILTINS: &[&'static dyn BuiltinClone] = &[
    &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,
    &builtin::var,
    &builtin::completion,
    &builtin::alias,
    &builtin::unalias,
    #[cfg(debug_assertions)]
    &builtin::debug,
    &builtin::terminfo,
    &builtin::bind::new(),
    &builtin::exit,
    &builtin::ct,
    &builtin::source,
    &builtin::Break,
    &builtin::Continue,
    &builtin::Here,
    &builtin::logo,
];

pub fn builtin_map() -> HashMap<BString, &'static dyn BuiltinClone> {
    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 BuiltinClone),
    Fun(Block),
    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
    }
}
