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("keepassdb") {
        c.keepassdb = matches.opt_str("keepassdb");
    }

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

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

    c.keepassfetch = matches.opt_present("keepassfetch");

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

    if matches.opt_present("wpinstfmt") {
        c.wpinst_fmt = true;
    }

    if matches.opt_present("wpuserfmt") {
        c.wpuser_fmt = true;
    }

    if matches.opt_present("wpconfigfmt") {
        c.wpuser_fmt = 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 matches.opt_present("salt") {
        c.salt = matches.opt_str("salt");
    }

    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) => Some(l),
            Err(_) => {
                eprintln!("cannot convert {} to number", n);
                std::process::exit(1);
            }
        }
    } else if totp_mode(matches) {
        Some(1)
    } else {
        None
    };

    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 totp_mode(matches) {
        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());
        }
    }

    if totp_mode(matches) && !matches.opt_present("totp") && env::var("TOTP").is_err() {
        eprintln!("cannot use totpfmt when --totp or TOTP are unset");
        std::process::exit(1);
    }
}

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", "alias for --format '%{md5} # %{password}\\n'");
    opts.optflag(
        "",
        "bcrypt",
        "alias for --format '%{bcrypt} # %{password}\\n'",
    );
    opts.optflag("", "des", "alias for --format '%{des} # %{password}\\n'");
    opts.optflag("", "sha1", "alias for --format '%{sha1} # %{password}\\n'");
    opts.optflag(
        "",
        "sha256",
        "alias for --format '%{sha256} # %{password}\\n'",
    );
    opts.optflag(
        "",
        "sha512",
        "alias for --format '%{sha512} # %{password}\\n'",
    );
    opts.optopt("", "loopdelay", "print and loop delay", "SECONDS");

    opts.optopt("", "keepassdb", "keepassdb to store", "PATH");
    opts.optopt(
        "",
        "keepassphrase",
        "keepassdb phrase, or use KEEPASSPHRASE environment",
        "PHRASE",
    );

    opts.optflag(
        "",
        "keepassfetch",
        "use the password from keepass, useful for account generation strings",
    );

    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.optopt("", "salt", "set a salt string", "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.optflag("", "wpconfigfmt", "wp cli config statements");
    opts.optflag("", "wpinstfmt", "wp cli inst statements");
    opts.optflag("", "wpuserfmt", "wp user table update");
    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, or base32",
        "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() || matches.opt_present("totpfmt") {
            print!("\x1b[2J\x1b[H");
        }
        main_loop(&matches, &mut c);
        if c.loopdelay.is_some() || matches.opt_present("totpfmt") {
            sleep((c.loopdelay.unwrap_or(1) * 1000).into());
        } else {
            break;
        }
    }
}
