use std::collections::HashMap;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;

use crate::parse::Ast;
use crate::*;

mod builtin;

pub fn run(se: &Session, cmd: Vec<u8>) {
    run_command(&se.raw, cmd);
}

pub trait Builtin: Send + Sync {
    fn name(&self) -> &str;

    /// quick synchronous call, `cd` for example
    fn mod_session(&self, session: &mut Session, args: &[BString]) {}

    /// potentially long, pipelineable thread, builtin `cat` for example
    fn io(
        &self,
        args: &[BString],
        stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> std::io::Result<()> {
        Ok(())
    }
}

const BUILTINS: &[&'static dyn Builtin] = &[
    &builtin::Cd,
    &builtin::Clear,
    #[cfg(debug_assertions)]
    &builtin::Re,
];

pub struct CommandDispatch {
    map: HashMap<BString, CommandKind>,
}

impl CommandDispatch {
    fn new() -> Self {
        let mut map = HashMap::new();

        // all the commands from PATH
        let path = std::env::var_os("PATH").unwrap();
        for p in path.as_bytes().split(|x| *x == b':').rev() {
            let p = PathBuf::from(OsStr::from_bytes(p));
            let Ok(entries) = fs::read_dir(p) else {
                continue;
            };

            for entry in entries {
                let Ok(entry) = entry else { continue };
                let Ok(meta) = entry.metadata() else {
                    continue;
                };

                if !meta.is_file() {
                    continue;
                }

                let perms = meta.permissions();
                let mode = perms.mode();

                // Check if any execute bit is set (owner/group/other)
                if mode & 0o111 == 0 {
                    continue;
                }

                // insert into our command map, mind the .rev() on the iterator above s.t. correct precedence is had
                map.insert(
                    entry.file_name().as_bytes().to_vec(),
                    CommandKind::Path(entry.path()),
                );
            }
        }

        // builtins
        for &b in BUILTINS {
            map.insert(b.name().as_bytes().to_vec(), CommandKind::Builtin(b));
        }

        todo!()
    }

    fn get(&self, cmd: &bstr) -> Option<CommandKind> {
        if cmd.starts_with(b"/") || cmd.starts_with(b"./") {
            Some(CommandKind::Path(PathBuf::from(OsStr::from_bytes(cmd))))
        } else {
            self.map.get(cmd).cloned()
        }
    }
}

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

fn run_command(raw: &ScopedRawMode, line: Vec<u8>) {
    let parsed = parse::do_parse(&line);

    let parsed = match parsed {
        Ok(p) => p,
        Err(err) => {
            println!("{line:?}");
            print!("{err:?}\r\n{PROMPT}");
            return;
        }
    };

    let Ast::Pipes(pipes) = parsed else {
        todo!("can only handle pipes");
    };

    let mut children: Vec<Child> = Vec::new();
    let mut prev_stdout = None;
    let mut spawn_error = false;

    raw.disable();

    for (i, cmd) in pipes.cmds.iter().enumerate() {
        let mut command = Command::new(OsStr::from_bytes(&cmd.cmd));
        for arg in cmd.args.iter() {
            command.arg(OsStr::from_bytes(arg));
        }

        if let Some(stdout) = prev_stdout.take() {
            command.stdin(Stdio::from(stdout));
        }

        if i < pipes.cmds.len() - 1 {
            command.stdout(Stdio::piped());
        }

        let mut child = match command.spawn() {
            Ok(c) => c,
            Err(e) => {
                println!("failed to spawn {:?} - {e:?}", OsStr::from_bytes(&cmd.cmd));
                spawn_error = true;
                break;
            }
        };

        prev_stdout = child.stdout.take();

        children.push(child);
    }

    let status_string;

    if spawn_error {
        for child in children.iter_mut() {
            if let Err(e) = child.kill() {
                println!("failed to kill child - {e:?}");
            }
        }
        status_string = "ERR".into();
    } else {
        let mut code = 0;
        for mut child in children {
            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 {
            status_string = String::new();
        } else {
            status_string = format!("{code}");
        }
    }

    raw.enable();

    print!("\r{status_string}{PROMPT}");
}
