use getopts::Options;
use pwtool::*;
use std::env;

const VERSION: &str = env!("CARGO_PKG_VERSION");

fn print_usage(program: &str, opts: Options) {
    let brief = format!("Usage: {} [options]", program);
    print!("{}", opts.usage(&brief));
}

fn set_config_options(
    matches: &getopts::Matches,
    c: &mut Config,
    args: Vec<String>,
    opts: Options,
) {
    fn set_totp_algo(matches: &getopts::Matches, c: &mut Config) {
        if matches.opt_present("sha256") {
            c.totp_algo = TotpAlgo::Sha256;
        }
        if matches.opt_present("sha512") {
            c.totp_algo = TotpAlgo::Sha512;
        }
    }

    if matches.opt_present("version") {
        println!("pwtool {VERSION}");
        std::process::exit(0);
    }

    if matches.opt_present("words") {
        c.word_list = Some("/usr/share/dict/words".to_string());
    }

    if matches.opt_present("createdatabase") {
        c.create_database = true;
    }

    if matches.opt_present("loopdelay") {
        c.loopdelay = match matches
            .opt_str("loopdelay")
            .as_ref()
            .unwrap()
            .parse::<u32>()
        {
            Ok(x) => Some(x),
            Err(x) => {
                eprintln!(
                    "{} is not parsable as a number: {}",
                    matches.opt_str("loopdelay").unwrap(),
                    x
                );
                print_usage(&args[0], opts);
                std::process::exit(3);
            }
        }
    }

    if matches.opt_present("wordsfile") {
        let words = matches.opt_str("wordsfile").unwrap();
        c.word_list = Some(words);
    }

    let mut found = false;
    for opt in [
        "onlynumeric",
        "onlyalpha",
        "onlyuppercase",
        "onlylowercase",
        "onlyextended",
    ] {
        if matches.opt_present(opt) {
            if found {
                eprintln!("cannot have multiple --only* options");
                std::process::exit(1);
            }
            found = true;
        }
    }

    if matches.opt_present("onlynumeric") {
        c.pw_type = Some(PwClass::Num as u32);
    }
    if matches.opt_present("onlyalpha") {
        c.pw_type = Some(PwClass::Alpha as u32);
    }
    if matches.opt_present("onlyuppercase") {
        c.pw_type = Some(PwClass::Upper as u32);
    }
    if matches.opt_present("onlylowercase") {
        c.pw_type = Some(PwClass::Lower as u32);
    }
    if matches.opt_present("onlyextended") {
        c.pw_type = Some(PwClass::Ext as u32);
    }
    if matches.opt_present("onlyextended") {
        c.pw_type = Some(PwClass::Ext as u32);
    }

    if matches.opt_present("help") {
        print_usage(&args[0], opts);

        std::process::exit(0);
    }

    if env::var("PASSWORD").is_ok() {
        c.password = Some(env::var("PASSWORD").unwrap());
    }

    if matches.opt_present("password") {
        c.password = matches.opt_str("password");
    }

    if matches.opt_present("username") {
        c.username = matches.opt_str("username");
    }

    if matches.opt_present("database") {
        c.database = matches.opt_str("database");
    }

    if matches.opt_present("servername") {
        c.servername = matches.opt_str("servername");
    }

    if env::var("TOTP").is_ok() {
        c.totp_key = Some(env::var("TOTP").unwrap());
        set_totp_algo(matches, c);
    }

    if matches.opt_present("totp") {
        c.totp_key = matches.opt_str("totp");
        set_totp_algo(matches, c);
    }

    if matches.opt_present("totpstep") {
        c.totp_step = match matches.opt_str("totpstep").as_ref().unwrap().parse::<u64>() {
            Ok(x) => Some(x),
            Err(x) => {
                eprintln!(
                    "{} is not parsable as a number: {}",
                    matches.opt_str("totpstep").unwrap(),
                    x
                );
                print_usage(&args[0], opts);
                std::process::exit(3);
            }
        };
    }

    if matches.opt_present("totpseconds") {
        c.totp_seconds = match matches
            .opt_str("totpseconds")
            .as_ref()
            .unwrap()
            .parse::<u64>()
        {
            Ok(x) => Some(x),
            Err(x) => {
                eprintln!(
                    "{} is not parsable as a number: {}",
                    matches.opt_str("totpseconds").unwrap(),
                    x
                );
                print_usage(&args[0], opts);
                std::process::exit(3);
            }
        };
    }

    c.number = if matches.opt_present("number") {
        let n = matches.opt_str("number").unwrap();

        match n.parse::<u32>() {
            Ok(l) => l,
            Err(_) => {
                eprintln!("cannot convert {} to number", n);
                std::process::exit(1);
            }
        }
    } else if c.totp_key.is_some() {
        1
    } else {
        20
    };

    if matches.opt_present("numeric") {
        c.pw_type = Some(match c.pw_type {
            Some(s) => s | PwClass::Num as u32,
            None => {
                PwClass::Alpha as u32
                    | PwClass::Lower as u32
                    | PwClass::Upper as u32
                    | PwClass::Num as u32
            }
        });
    }

    if matches.opt_present("alpha") {
        c.pw_type = Some(match c.pw_type {
            Some(s) => s | PwClass::Alpha as u32,
            None => {
                PwClass::Alpha as u32
                    | PwClass::Lower as u32
                    | PwClass::Upper as u32
                    | PwClass::Num as u32
            }
        });
    }

    if matches.opt_present("extended") {
        c.pw_type = Some(match c.pw_type {
            Some(s) => s | PwClass::Ext as u32,
            None => {
                PwClass::Alpha as u32
                    | PwClass::Lower as u32
                    | PwClass::Upper as u32
                    | PwClass::Num as u32
                    | PwClass::Ext as u32
            }
        });
    }

    if matches.opt_present("uppercase") {
        c.pw_type = Some(match c.pw_type {
            Some(s) => s | PwClass::Upper as u32,
            None => {
                PwClass::Alpha as u32
                    | PwClass::Lower as u32
                    | PwClass::Upper as u32
                    | PwClass::Num as u32
            }
        });
    }

    if matches.opt_present("lowercase") {
        c.pw_type = Some(match c.pw_type {
            Some(s) => s | PwClass::Lower as u32,
            None => {
                PwClass::Alpha as u32
                    | PwClass::Lower as u32
                    | PwClass::Upper as u32
                    | PwClass::Num as u32
            }
        });
    }

    if matches.opt_present("help") {
        print_usage(&args[0], opts);
        std::process::exit(0);
    }

    c.len = if matches.opt_present("length") {
        let n = matches.opt_str("length").unwrap();

        match n.parse::<u32>() {
            Ok(l) => Some(l),
            Err(_) => {
                eprintln!("cannot convert {} to number", n);
                std::process::exit(1);
            }
        }
    } else if c.word_list.is_some() {
        Some(3)
    } else if c.totp_key.is_some() {
        Some(6)
    } else {
        Some(10)
    };

    c.spaces = !matches.opt_present("nospaces");

    for digest in ["bcrypt", "md5", "des", "sha1", "sha256", "sha512"] {
        if matches.opt_present(digest) {
            c.digest = Some(digest.to_string());
        }
    }
}

fn main() {
    let mut c = Config::new();

    let args: Vec<String> = env::args().collect();
    let mut opts = Options::new();

    opts.optflag("h", "help", "display help");
    opts.optflag("", "alpha", "use alpha characters (default)");
    opts.optflag("", "numeric", "use numeric characters (default)");
    opts.optflag("", "extended", "use extended characters");
    opts.optflag("", "lowercase", "use lowercase characters (default)");
    opts.optflag("", "uppercase", "use uppercase characters (default)");

    opts.optflag("", "onlyuppercase", "use uppercase characters");
    opts.optflag("", "onlylowercase", "use lowercase characters");
    opts.optflag("", "onlynumeric", "use numeric characters");
    opts.optflag("", "onlyextended", "use extended characters");
    opts.optflag("", "onlyalpha", "use alpha characters");

    opts.optflag("", "md5", "use MD5");
    opts.optflag("", "bcrypt", "use bcrypt");
    opts.optflag("", "des", "use traditional unix crypt");
    opts.optflag("", "sha1", "use SHA1");
    opts.optflag("", "sha256", "use SHA256");
    opts.optflag("", "sha512", "use SHA512");
    opts.optopt("", "loopdelay", "print and loop delay", "SECONDS");

    opts.optflag("", "mysqlfmt", "alias for --format \"%{mysqlfmt}\\n\"");
    opts.optflag(
        "",
        "mysqluserfmt",
        "alias for --format \"%{mysqluserfmt}\\n\"",
    );
    opts.optflag("", "pgfmt", "alias for --format \"%{pgfmt}\\n\"");
    opts.optflag("", "userfmt", "alias for --format \"%{userfmt}\\n\"");
    opts.optflag("", "usermodfmt", "alias for --format \"%{usermodfmt}\\n\"");
    opts.optflag("", "htauthfmt", "alias for --format \"%{htauthfm}\\n\"");
    opts.optflag("", "nginxfmt", "alias for --format \"%{nginxfmt}\\n\"");
    opts.optflag("", "apachefmt", "alias for --format \"%{apachefmt}\\n\"");
    opts.optflag("", "totpfmt", "alias for --format \"%{totpfmt}\\n\"");

    opts.optopt("", "username", "for %{username} formatter", "STRING");
    opts.optopt("", "database", "for %{database} formatter", "STRING");
    opts.optopt("", "servername", "for %{servername} formatter", "STRING");
    opts.optopt(
        "",
        "password",
        "use input and don't generate, PASSWORD can also be an environment variable",
        "STRING",
    );
    opts.optflag(
        "",
        "createdatabase",
        "when using --pgfmt or --mysql*fmt, prefix with a create database statement",
    );
    opts.optopt(
        "",
        "totp",
        "use TOTP key (base32), TOTP can also be an environment variable",
        "STRING",
    );
    opts.optopt("", "totpstep", "use TOTP step (default 30)", "SECONDS");
    opts.optopt(
        "",
        "totpseconds",
        "use SECONDS as time reference",
        "SECONDS",
    );

    opts.optflag("v", "version", "display version");
    opts.optopt("n", "number", "number of passwords", "NUMBER");
    opts.optopt(
        "l",
        "length",
        "length of passwords (default is three with a wordlist)",
        "NUMBER",
    );
    opts.optflag("w", "words", "use default wordlist");
    opts.optflag("", "nospaces", "don't join words with spaces");
    opts.optopt("", "wordsfile", "use wordsfile", "FILE");
    opts.optopt(
        "",
        "format",
        "output using a string: %{VAL} where VAL is md5, bcrypt, des, sha1, sha256, sha512, mysql, postgres, database, password or username.\nmysqlfmt, pgfmt, userfmt, htauthfmt\ntotp for the totp digits, totpprogress for bar graph, totpsecs for remaining secs, totpalgo for the algorithm and totpsecsmax for step",
        "FORMAT",
    );

    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            eprintln!("{}", f);
            std::process::exit(1);
        }
    };

    set_config_options(&matches, &mut c, args, opts);
    set_wordlist(&mut c);

    loop {
        if c.loopdelay.is_some() {
            print!("\x1b[2J\x1b[H");
        }
        main_loop(&matches, &c);
        if c.loopdelay.is_some() {
            sleep((c.loopdelay.unwrap() * 1000).into());
        } else {
            break;
        }
    }
}
