use std::{collections::BTreeMap, env, io::Write, path::Path, process::ExitCode, sync::RwLock};

use crate::exec::{InheritStderr, InheritStdout, Input, Outcome, Output};

pub type Builtin = fn(Args, Vec<String>, Envs) -> Outcome;

pub struct Args {
    pub stdin: Input,
    pub stdout: Output<InheritStdout>,
    pub stderr: Output<InheritStderr>,
}
pub struct Envs(Vec<(String, String)>);

impl From<Vec<(String, String)>> for Envs {
    fn from(value: Vec<(String, String)>) -> Self {
        Self(value)
    }
}

static BUILTINS: RwLock<BTreeMap<String, Builtin>> = RwLock::new(BTreeMap::new());

pub fn get(program: &str) -> Option<Builtin> {
    let builtins = BUILTINS.read().unwrap();
    builtins.get(program).copied()
}

pub fn register(name: &str, function: Builtin) {
    BUILTINS.write().unwrap().insert(name.into(), function);
}

pub fn b_test(_io: Args, args: Vec<String>, _env: Envs) -> Outcome {
    if args.is_empty() {
        return Outcome::err();
    }
    let res = match &args[..] {
        [x] => !x.is_empty(),
        [op, x] => match op.as_str() {
            "-b" => todo!(),
            "-c" => todo!(),
            "-d" => Path::new(x).is_dir(),
            "-e" => Path::new(x).exists(),
            "-f" => Path::new(x).is_file(),
            "-g" => todo!(),
            "-h" => todo!(),
            "-L" => Path::new(x).is_symlink(),
            "-n" => todo!(),
            "-p" => todo!(),
            "-r" => todo!(),
            "-S" => todo!(),
            "-s" => todo!(),
            "-t" => todo!(),
            "-u" => todo!(),
            "-w" => todo!(),
            "-x" => todo!(),
            "-z" => todo!(),
            _ => false,
        },
        [x, op, y] => match op.as_str() {
            "-nt" => {
                if let (Ok(t0), Ok(t1)) = (
                    Path::new(x).metadata().and_then(|m| m.modified()),
                    Path::new(y).metadata().and_then(|m| m.modified()),
                ) {
                    t0 > t1
                } else {
                    false
                }
            }
            "-ot" => {
                if let (Ok(t0), Ok(t1)) = (
                    Path::new(x).metadata().and_then(|m| m.modified()),
                    Path::new(y).metadata().and_then(|m| m.modified()),
                ) {
                    t0 < t1
                } else {
                    false
                }
            }
            "-ef" => todo!(),
            "-eq" => todo!(),
            "-ne" => todo!(),
            "-gt" => todo!(),
            "-lt" => todo!(),
            "-ge" => todo!(),
            "-le" => todo!(),
            _ => false,
        },
        _ => todo!(),
    };

    match res {
        true => Outcome::ok(),
        false => Outcome::err(),
    }
}

fn b_echo(mut io: Args, args: Vec<String>, _env: Envs) -> Outcome {
    for (i, arg) in args.iter().enumerate() {
        if i != 0 {
            write!(io.stdout, " ").ok();
        }
        write!(io.stdout, "{arg}").ok();
    }
    writeln!(io.stdout).ok();
    Outcome::ok()
}

fn b_cd(mut io: Args, args: Vec<String>, _env: Envs) -> Outcome {
    if args.len() != 1 {
        writeln!(io.stderr, "`cd` requires one argument").ok();
        return Outcome::err();
    }
    let path = &args[0];
    match env::set_current_dir(path) {
        Ok(()) => Outcome::ok(),
        Err(error) => {
            writeln!(io.stderr, "{path}: {error}").ok();
            Outcome::err()
        }
    }
}

fn b_pwd(mut io: Args, _args: Vec<String>, _env: Envs) -> Outcome {
    match env::current_dir() {
        Ok(path) => {
            writeln!(io.stdout, "{}", path.display()).ok();
            Outcome::ok()
        }
        Err(error) => {
            writeln!(io.stderr, "pwd: {error}").ok();
            Outcome::err()
        }
    }
}

fn b_exit(mut io: Args, args: Vec<String>, _env: Envs) -> Outcome {
    let code = match args.len() {
        0 => ExitCode::SUCCESS,
        1 => todo!(),
        _ => {
            writeln!(io.stderr, "Usage: exit [<code>]").ok();
            return Outcome::err();
        }
    };

    Outcome::ExitShell(code)
}

fn b_export(_io: Args, _args: Vec<String>, env: Envs) -> Outcome {
    for (key, value) in env.0 {
        env::set_var(key, value);
    }
    Outcome::ok()
}

pub fn register_default() {
    register("echo", b_echo);
    register("cd", b_cd);
    register("pwd", b_pwd);
    register("exit", b_exit);
    register("export", b_export);
}
