use base32::*;
use base64::prelude::*;
use hmac::{Hmac, Mac};
use pbkdf2::pbkdf2_hmac;
use pwhash::*;
use rand::distributions::Alphanumeric;
use rand::thread_rng;
use rand::Rng;
use sha1::{Digest, Sha1};
use sha2::Sha256;
use sha2::Sha512;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::thread;
use std::time::Duration;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

#[derive(Clone)]
pub enum TotpAlgo {
    Sha1,
    Sha256,
    Sha512,
}

#[derive(Clone)]
pub struct Config {
    pub number: u32,
    pub len: Option<u32>,
    pub pw_type: Option<u32>,
    pub word_list: Option<String>,
    pub words: Option<Vec<String>>,
    pub username: Option<String>,
    pub database: Option<String>,
    pub create_database: bool,
    pub spaces: bool,
    pub digest: Option<String>,
    pub servername: Option<String>,
    pub totp_key: Option<String>,
    pub totp_step: Option<u64>,
    pub totp_algo: TotpAlgo,
    pub totp_seconds: Option<u64>,
    pub password: Option<String>,
    pub loopdelay: Option<u32>,
}

impl Config {
    pub fn new() -> Config {
        Config {
            len: None,
            pw_type: None,
            number: 20,
            word_list: None,
            words: None,
            username: None,
            database: None,
            create_database: false,
            spaces: true,
            digest: None,
            servername: None,
            totp_key: None,
            totp_step: Some(30),
            totp_seconds: None,
            password: None,
            totp_algo: TotpAlgo::Sha1,
            loopdelay: None,
        }
    }

    pub fn digest(&self) -> String {
        if self.digest.is_none() {
            return "sha256".to_string();
        }
        self.digest.as_ref().unwrap().to_string()
    }

    pub fn totp_seconds(&self) -> u64 {
        if self.totp_seconds.is_some() {
            self.totp_seconds.unwrap()
        } else {
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_secs()
        }
    }
}

impl Default for Config {
    fn default() -> Self {
        Self::new()
    }
}

pub enum PwClass {
    Num = 1 << 0,
    Alpha = 1 << 1,
    Ext = 1 << 2,
    Lower = 1 << 3,
    Upper = 1 << 4,
}

pub fn valid_word(s: &str, word_set: Option<u32>) -> bool {
    if word_set.is_some() {
        let mut valid = false;

        if word_set.as_ref().unwrap() & PwClass::Num as u32 != 0 {
            for j in s.chars() {
                if j.is_ascii_digit() {
                    valid = true;
                }
            }
        }

        if word_set.as_ref().unwrap() & PwClass::Lower as u32 != 0 {
            for j in s.chars() {
                if j.is_ascii_lowercase() {
                    valid = true;
                }
            }
        }

        if word_set.as_ref().unwrap() & PwClass::Upper as u32 != 0 {
            for j in s.chars() {
                if j.is_ascii_uppercase() {
                    valid = true;
                }
            }
        }

        if word_set.as_ref().unwrap() & PwClass::Ext as u32 != 0 {
            for j in s.chars() {
                if !j.is_ascii_digit() && !j.is_ascii_lowercase() && !j.is_ascii_uppercase() {
                    valid = true;
                }
            }
        }
        return valid;
    };

    true
}

pub fn prng_string(c: &Config) -> String {
    let mut rng = rand::thread_rng();

    if c.word_list.is_some() {
        let mut phrase = vec![];

        let words_len = c.words.as_ref().unwrap().len();
        if c.words.as_ref().unwrap().is_empty() {
            eprintln!("no words to process");
            std::process::exit(1);
        }

        for _j in 0..c.len.unwrap_or(15) {
            phrase.push(
                c.words.as_ref().unwrap()[rng.gen_range(0..words_len)]
                    .clone()
                    .to_string(),
            );
        }

        let mut phrase = phrase.join(if c.spaces { " " } else { "" });

        if c.pw_type.is_some() {
            let set = c.pw_type.as_ref().unwrap();
            if set & PwClass::Lower as u32 != 0 {
                phrase = phrase.to_lowercase();
            }

            if set & PwClass::Upper as u32 != 0 {
                phrase = phrase.to_uppercase();
            }
        }

        return phrase;
    }

    let mut set = c.pw_type;
    if set.is_none() {
        set = Some(
            PwClass::Num as u32
                | PwClass::Alpha as u32
                | PwClass::Lower as u32
                | PwClass::Upper as u32,
        );
    }

    let set = set.unwrap();
    let mut chars = "".to_string();
    if set & PwClass::Num as u32 != 0 {
        chars += "0123456789";
    };

    if set & PwClass::Alpha as u32 != 0 {
        chars += "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    };

    if set & PwClass::Lower as u32 != 0 {
        chars += "abcdefghijklmnopqrstuvwxyz";
    };

    if set & PwClass::Upper as u32 != 0 {
        chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    };

    if set & PwClass::Ext as u32 != 0 {
        chars += r#"!"$%^&*()-={}[]:;@'<>,./\|"#;
    };

    let one_char = || chars.chars().nth(rng.gen_range(0..chars.len())).unwrap();
    std::iter::repeat_with(one_char)
        .take(c.len.unwrap_or(15) as usize)
        .collect()
}

pub fn gen_salt_str(chars: usize) -> String {
    let rng = thread_rng();
    rng.sample_iter(&Alphanumeric)
        .take(chars)
        .map(|x| x as char)
        .collect()
}

pub fn postgres_pass(pw: &str) -> String {
    let salt_size = 16;
    let iterations = 4096;

    let salt = gen_salt_str(salt_size);
    let mut key = [0u8; 32];
    pbkdf2_hmac::<Sha256>(pw.as_bytes(), salt.as_bytes(), iterations, &mut key);

    let mut client_key = Hmac::<Sha256>::new_from_slice(&key).unwrap();
    client_key.update(b"Client Key");
    let client_result = client_key.finalize();

    let mut server_key = Hmac::<Sha256>::new_from_slice(&key).unwrap();
    server_key.update(b"Server Key");
    let server_result = server_key.finalize();

    let mut stored_key = Sha256::new();
    stored_key.update(client_result.clone().into_bytes());
    let stored_key = stored_key.finalize();

    let bstored_key = BASE64_STANDARD.encode(stored_key);
    let bsalt = BASE64_STANDARD.encode(salt);
    let bserver_key = BASE64_STANDARD.encode(server_result.clone().into_bytes());

    format!("SCRAM-SHA-256${iterations}:{bsalt}${bstored_key}:{bserver_key}")
}

pub fn mysql_pass(pw: &str) -> String {
    let mut h = Sha1::new();
    h.update(pw);
    let r = h.finalize();
    let mut h = Sha1::new();
    h.update(r);
    let r = h.finalize();
    format!("*{:x}", r).to_string()
}

// https://github.com/ansible-collections/community.mysql/issues/621#issuecomment-2051837825
pub fn to64(mut v: i32, n: i32) -> String {
    let mut index64: Vec<u8> = vec![];
    index64.push(b'.');
    index64.push(b'/');
    for i in 48..58 {
        index64.push(i);
    }
    for i in 65..91 {
        index64.push(i);
    }
    for i in 97..123 {
        index64.push(i);
    }

    let mut result = "".to_string(); // make a vec of u8
    let mut nn = n;
    while nn > 0 {
        nn -= 1;
        result.push(index64[v as usize & 0x3f] as char);
        v >>= 6;
    }

    String::from_utf8_lossy(result.as_bytes()).to_string()
}

pub fn mysql_caching_sha2_pass_salt(pw: &str, salt: &str, iterations: i32) -> String {
    let num_bytes = 32;

    let key = pw.as_bytes();
    let salt = salt.as_bytes();
    let mut d1: Vec<u8> = vec![];
    d1.extend(key);
    d1.extend(salt);
    d1.extend(key);

    let mut stored_key = Sha256::new();
    stored_key.update(d1);
    let digest_b = stored_key.clone().finalize();

    let mut tmp: Vec<u8> = vec![];
    tmp.extend(key);
    tmp.extend(salt);

    for i in (0..=key.len()).rev().step_by(num_bytes) {
        tmp.extend(if i > num_bytes {
            digest_b.as_slice()
        } else {
            &digest_b[0..i]
        });
    }

    let mut i = key.len();
    while i > 0 {
        tmp.extend(if i & 1 != 0 {
            digest_b.as_slice()
        } else {
            key as &[u8]
        });
        i >>= 1;
    }

    let mut digest_a_hash = Sha256::new();
    digest_a_hash.update(tmp);
    let digest_a = digest_a_hash.clone().finalize();

    let mut tmp: Vec<u8> = vec![];
    for _ in 0..key.len() {
        tmp.extend(key);
    }

    let mut digest_dp_hash = Sha256::new();
    digest_dp_hash.update(tmp);
    let digest_dp = digest_dp_hash.finalize();

    let mut byte_sequence_p: Vec<u8> = vec![];
    for i in (0..=key.len()).rev().step_by(num_bytes) {
        byte_sequence_p.extend(if i > num_bytes {
            digest_dp.as_slice()
        } else {
            &digest_dp[0..i]
        });
    }

    let mut tmp: Vec<u8> = vec![];
    let til = 16 + (digest_a[0] as i32);

    for _ in 0..til {
        tmp.extend(salt);
    }

    let mut digest_ds_hash = Sha256::new();
    digest_ds_hash.update(tmp);
    let digest_ds = digest_ds_hash.finalize();

    let mut byte_sequence_s: Vec<u8> = vec![];
    for i in (0..=salt.len()).rev().step_by(num_bytes) {
        byte_sequence_s.extend(if i > num_bytes {
            digest_ds.as_slice()
        } else {
            &digest_ds[0..i]
        });
    }

    let mut digest_c = digest_a;

    for i in 0..iterations * 1000 {
        tmp = if i & 1 > 0 {
            byte_sequence_p.clone()
        } else {
            (&digest_c as &[u8]).to_vec()
        };
        if i % 3 > 0 {
            tmp.extend(byte_sequence_s.clone());
        }
        if i % 7 > 0 {
            tmp.extend(byte_sequence_p.clone());
        }
        tmp.extend(if i & 1 > 0 {
            digest_c.as_slice()
        } else {
            &byte_sequence_p as &[u8]
        });

        let mut digest_c_hash = Sha256::new();
        digest_c_hash.update(tmp);
        digest_c = digest_c_hash.finalize();
    }

    let inc1 = 10;
    let inc2 = 21;
    let m = 30;
    let end = 0;

    let mut i = 0;
    let mut tmp = "".to_string();

    loop {
        tmp.push_str(&to64(
            ((digest_c[i] as i32) << 16)
                | (((digest_c[(i + inc1) % m]) as i32) << 8)
                | (digest_c[(i + inc1 * 2) % m]) as i32,
            4,
        ));
        i = (i + inc2) % m;

        if i == end {
            break;
        }
    }

    tmp.push_str(&to64(
        ((digest_c[31] as i32) << 8) | (digest_c[30] as i32),
        3,
    ));

    tmp
}

pub fn mysql_caching_sha2_pass(pw: &str) -> String {
    let salt_size = 20;
    let salt = gen_salt_str(salt_size);
    let count = 5;

    let st = mysql_caching_sha2_pass_salt(pw, &salt, count);
    format!(
        "0x{}",
        hex::encode(format!("$A${count:>03}${salt}{st}")).to_uppercase()
    )
}

pub fn useradd_format(c: &Config) -> String {
    let digest = c.digest();
    format!("useradd -m -s /bin/bash -p '%{{{digest}}}' %{{username}}").to_string()
}

pub fn usermod_format(c: &Config) -> String {
    let digest = c.digest();
    format!("usermod -p '%{{{digest}}}' %{{username}}").to_string()
}

pub fn mysql_format() -> String {
    "grant all privileges on %{database}.* to %{username}@'%' identified with mysql_native_password as '%{mysql}';".to_string()
}

pub fn mysql_user_format() -> String {
    "create user %{username}@'%'; alter user %{username}@'%' identified with 'caching_sha2_password' as %{mysql_caching_sha2}; grant all privileges on %{database}.* to %{username}@'%';".to_string()
}

pub fn postgres_format() -> String {
    "create user %{username} password '%{postgres}';".to_string()
}

pub fn htauth_format(c: &Config) -> String {
    let digest = if c.digest.is_none() {
        "md5".to_string()
    } else {
        c.digest()
    };
    format!("%{{username}}:%{{{digest}}}").to_string()
}

#[allow(deprecated)]
pub fn process_format_string(format_string: &mut String, c: &Config, pw: &str) -> String {
    if format_string.contains("%{userfmt}") {
        *format_string = format_string.replace("%{userfmt}", &useradd_format(c).to_string());
    }
    if format_string.contains("%{usermodfmt}") {
        *format_string = format_string.replace("%{usermodfmt}", &usermod_format(c).to_string());
    }
    if format_string.contains("%{mysqlfmt}") {
        *format_string = format_string.replace("%{mysqlfmt}", &mysql_format().to_string());
    }
    if format_string.contains("%{mysqluserfmt}") {
        *format_string = format_string.replace("%{mysqluserfmt}", &mysql_user_format().to_string());
    }
    if format_string.contains("%{pgfmt}") {
        *format_string = format_string.replace("%{pgfmt}", &postgres_format().to_string());
    }
    if format_string.contains("%{htauthfmt}") {
        *format_string = format_string.replace("%{htauthfmt}", &htauth_format(c).to_string());
    }

    if format_string.contains("%{totpfmt}") {
        *format_string = format_string.replace("%{totpfmt}", "%{totp} [%{totpprogress}]");
    }

    if format_string.contains("%{md5}") {
        *format_string = format_string.replace("%{md5}", &md5_crypt::hash(pw).unwrap().to_string());
    }
    if format_string.contains("%{bcrypt}") {
        *format_string = format_string.replace("%{bcrypt}", &bcrypt::hash(pw).unwrap().to_string());
    }
    if format_string.contains("%{des}") {
        *format_string =
            format_string.replace("%{des}", &unix_crypt::hash(pw).unwrap().to_string());
    }
    if format_string.contains("%{sha1}") {
        *format_string =
            format_string.replace("%{sha1}", &sha1_crypt::hash(pw).unwrap().to_string());
    }
    if format_string.contains("%{sha256}") {
        *format_string =
            format_string.replace("%{sha256}", &sha256_crypt::hash(pw).unwrap().to_string());
    }
    if format_string.contains("%{sha512}") {
        *format_string =
            format_string.replace("%{sha512}", &sha512_crypt::hash(pw).unwrap().to_string());
    }
    if format_string.contains("%{password}") {
        *format_string = format_string.replace("%{password}", pw);
    }
    if format_string.contains("%{username}") && c.username.is_some() {
        *format_string = format_string.replace("%{username}", c.username.as_ref().unwrap());
    }
    if format_string.contains("%{database}") && c.database.is_some() {
        *format_string = format_string.replace("%{database}", c.database.as_ref().unwrap());
    }

    if format_string.contains("%{mysql}") {
        *format_string = format_string.replace("%{mysql}", &mysql_pass(pw).to_string());
    }
    if format_string.contains("%{mysql_caching_sha2}") {
        *format_string = format_string.replace(
            "%{mysql_caching_sha2}",
            &mysql_caching_sha2_pass(pw).to_string(),
        );
    }

    if format_string.contains("%{postgres}") {
        *format_string = format_string.replace("%{postgres}", &postgres_pass(pw));
    }
    if format_string.contains("%{username}") && c.username.is_some() {
        *format_string = format_string.replace("%{username}", c.username.as_ref().unwrap());
    }
    if format_string.contains("%{database}") && c.database.is_some() {
        *format_string = format_string.replace("%{database}", c.database.as_ref().unwrap());
    }

    if format_string.contains("%{mysql}") {
        *format_string = format_string.replace("%{mysql}", &mysql_pass(pw));
    }

    if format_string.contains("%{servername}") && c.servername.is_some() {
        *format_string = format_string.replace("%{servername}", c.servername.as_ref().unwrap());
    }

    if format_string.contains("%{totp}") && c.totp_key.is_some() {
        *format_string = format_string.replace("%{totp}", &totp_string(c));
    }

    if format_string.contains("%{totpsecs}") && c.totp_key.is_some() {
        *format_string = format_string.replace("%{totpsecs}", &totp_secs_remaining(c).to_string());
    }

    if format_string.contains("%{totpsecsmax}") && c.totp_key.is_some() {
        *format_string = format_string.replace("%{totpsecsmax}", &c.totp_step.unwrap().to_string());
    }

    if format_string.contains("%{totpprogress}") && c.totp_key.is_some() {
        *format_string = format_string.replace("%{totpprogress}", &totp_progress(c));
    }

    if format_string.contains("%{totpalgo}") && c.totp_key.is_some() {
        *format_string = format_string.replace(
            "%{totpalgo}",
            match c.totp_algo {
                TotpAlgo::Sha1 => "sha1",
                TotpAlgo::Sha256 => "sha256",
                TotpAlgo::Sha512 => "sha512",
            },
        );
    }

    *format_string = format_string.replace(r#"\n"#, "\n");

    format_string.to_string()
}

pub fn create_pg_database(c: &Config) -> String {
    if c.create_database {
        let db = c.database.as_ref().unwrap_or(&"".to_string()).to_string();
        let username = c.username.as_ref().unwrap_or(&"".to_string()).to_string();
        return format!("create database {db}; alter database {db} owner to {username}; ")
            .to_string();
    }
    "".to_string()
}

pub fn create_mysql_database(c: &Config) -> String {
    if c.create_database {
        return format!(
            "create database {}; ",
            c.database.as_ref().unwrap_or(&"".to_string())
        )
        .to_string();
    }
    "".to_string()
}

pub fn create_nginx_vhost() -> String {
    let vhost = r#"
server {
    listen 80;
    listen [::]:80;
    server_name %{servername} www.%{servername};
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
    server_name %{servername} www.%{servername};

    access_log /var/log/nginx/%{servername}_access.log;
    error_log /var/log/nginx/%{servername}_error.log;

    include /etc/nginx/snippets/snakeoil.conf;

    location / {
    }
}
"#;
    vhost.to_string()
}

pub fn create_apache_vhost() -> String {
    let vhost = r#"
<VirtualHost *:80>
    ServerName %{servername}
    ServerAlias www.%{servername}

    DocumentRoot /home/%{username}/public_html

    ErrorLog ${APACHE_LOG_DIR}/%{servername}_error.log
    CustomLog ${APACHE_LOG_DIR}/%{servername}_access.log combined

    <Directory "/home/%{username}/public_html">
        Require all granted
        AllowOverride All
    </Directory>
</VirtualHost>
"#;
    vhost.to_string()
}

pub fn totp_string(c: &Config) -> String {
    fn to_bytes(n: u64) -> [u8; 8] {
        let mask: u64 = 0xff;
        let mut bytes: [u8; 8] = [0; 8];
        (0..8).for_each(|i| bytes[7 - i] = (mask & (n >> (i * 8))) as u8);
        bytes
    }

    fn totp_maker(c: &Config, pw: &[u8]) -> String {
        let mut hash: Vec<u8> = vec![];

        match c.totp_algo {
            TotpAlgo::Sha1 => {
                let mut mac = Hmac::<Sha1>::new_from_slice(pw).unwrap();
                Hmac::<Sha1>::update(&mut mac, &to_bytes(c.totp_seconds() / c.totp_step.unwrap()));
                hash.extend_from_slice(&mac.finalize().into_bytes());
            }
            TotpAlgo::Sha256 => {
                let mut mac = Hmac::<Sha256>::new_from_slice(pw).unwrap();
                Hmac::<Sha256>::update(
                    &mut mac,
                    &to_bytes(c.totp_seconds() / c.totp_step.unwrap()),
                );
                hash.extend_from_slice(&mac.finalize().into_bytes());
            }
            TotpAlgo::Sha512 => {
                let mut mac = Hmac::<Sha512>::new_from_slice(pw).unwrap();
                Hmac::<Sha512>::update(
                    &mut mac,
                    &to_bytes(c.totp_seconds() / c.totp_step.unwrap()),
                );
                hash.extend_from_slice(&mac.finalize().into_bytes());
            }
        }

        let offset: usize = (hash.last().unwrap() & 0xf) as usize;
        let binary: u64 = (((hash[offset] & 0x7f) as u64) << 24)
            | ((hash[offset + 1] as u64) << 16)
            | ((hash[offset + 2] as u64) << 8)
            | (hash[offset + 3] as u64);

        format!(
            "{:01$}",
            binary % (10_u64.pow(c.len.unwrap_or(6))),
            c.len.unwrap_or(6) as usize
        )
    }

    let s = c
        .totp_key
        .as_ref()
        .unwrap()
        .trim()
        .to_lowercase()
        .to_string();
    let password: &[u8] = &match base32::decode(Alphabet::Rfc4648Lower { padding: false }, &s) {
        Some(x) => x,
        None => {
            eprintln!("Cannot base32 convert {}", &s);
            std::process::exit(1);
        }
    };

    totp_maker(c, password)
}

pub fn totp_secs_remaining(c: &Config) -> u64 {
    c.totp_step.unwrap() - c.totp_seconds() % c.totp_step.unwrap()
}

pub fn totp_progress_bar(c: &Config) -> String {
    let width: usize = 30;
    let percent: usize = width / (c.totp_step.unwrap() as usize);
    let remaining: usize = (c.totp_step.unwrap() as usize)
        - (c.totp_seconds() as usize) % (c.totp_step.unwrap() as usize);
    let progress = percent * remaining;
    let padding = width - progress;

    format!(
        "{empty:#<progress$}{empty: ^padding$}",
        empty = "",
        progress = progress,
        padding = padding,
    )
}

pub fn totp_progress(c: &Config) -> String {
    let mut ret = "".to_string();

    ret.push_str(&urgent_colour(totp_secs_remaining(c) as i32));
    ret.push_str(&totp_progress_bar(c));
    ret.push_str(&normal_colour());

    ret.to_string()
}

pub fn set_wordlist(c: &mut Config) {
    if c.word_list.is_some() {
        let word_list = c.word_list.as_ref().unwrap();
        if c.words.is_none() {
            let f = File::open(word_list);
            if f.is_err() {
                eprintln!("Cannot open {}: {}", word_list, f.err().unwrap());
                std::process::exit(1);
            }

            let f = f.unwrap();

            let mut line = String::new();
            let mut br = BufReader::new(f);
            let mut wv: Vec<String> = vec![];
            loop {
                line.clear();
                let l = br.read_line(&mut line);
                match l {
                    Ok(i) => {
                        if i == 0 {
                            break;
                        }
                    }
                    Err(_) => {
                        break;
                    }
                }

                // skip words with apostrophes etc

                if line.find('\'').is_some() {
                    continue;
                }

                let s = line.clone().trim().to_string();
                if s.is_empty() {
                    continue;
                }

                if !valid_word(&s, c.pw_type) {
                    continue;
                }

                wv.push(s);
            }
            c.words = Some(wv);
        }
    }
}

#[allow(deprecated)]
pub fn pw_loop(matches: &getopts::Matches, c: &Config) {
    let pw = (if c.password.is_some() {
        c.password.as_ref().unwrap().to_string()
    } else {
        (*prng_string(c)).to_string()
    })
    .to_string();
    let mut donefmtopt = false;

    if matches.opt_present("pgfmt") {
        let mut format_string =
            format!("%{{pgfmt}} {} -- # %{{password}}\n", create_pg_database(c)).to_string();
        format_string = process_format_string(&mut format_string, c, &pw);
        print!("{}", format_string);
        donefmtopt = true;
    }

    if matches.opt_present("mysqlfmt") {
        let mut format_string = format!(
            "{}%{{mysqlfmt}} -- # %{{password}}\n",
            create_mysql_database(c)
        )
        .to_string();
        format_string = process_format_string(&mut format_string, c, &pw);
        print!("{}", format_string);
        donefmtopt = true;
    }

    if matches.opt_present("mysqluserfmt") {
        let mut format_string = format!(
            "{}%{{mysqluserfmt}} -- # %{{password}}\n",
            create_mysql_database(c)
        )
        .to_string();
        format_string = process_format_string(&mut format_string, c, &pw);
        print!("{}", format_string);
        donefmtopt = true;
    }

    if matches.opt_present("htauthfmt") {
        let mut format_string = "# %{password}\n%{htauthfmt}\n".to_string();
        format_string = process_format_string(&mut format_string, c, &pw);
        print!("{}", format_string);
        donefmtopt = true;
    }

    if matches.opt_present("nginxfmt") {
        let mut format_string = create_nginx_vhost();
        format_string = process_format_string(&mut format_string, c, &pw);
        print!("{}", format_string);
        donefmtopt = true;
    }
    if matches.opt_present("apachefmt") {
        let mut format_string = create_apache_vhost();
        format_string = process_format_string(&mut format_string, c, &pw);
        print!("{}", format_string);
        donefmtopt = true;
    }

    if matches.opt_present("userfmt") {
        let mut format_string = "%{userfmt} # %{password}\n".to_string();
        format_string = process_format_string(&mut format_string, c, &pw);
        print!("{}", format_string);
        donefmtopt = true;
    }

    if matches.opt_present("usermodfmt") {
        let mut format_string = "%{usermodfmt} # %{password}\n".to_string();
        format_string = process_format_string(&mut format_string, c, &pw);
        print!("{}", format_string);
        donefmtopt = true;
    }

    if matches.opt_present("totpfmt") {
        let mut format_string = "%{totpfmt}\n".to_string();
        format_string = process_format_string(&mut format_string, c, &pw);
        print!("{}", format_string);
        donefmtopt = true;
    }

    if matches.opt_present("format") {
        let mut format_string = matches.opt_str("format").unwrap().to_string();

        if format_string.trim() == "useradd" {
            format_string = "%{userfmt} # %{password}\n".to_string();
        }
        if format_string.trim() == "usermod" {
            format_string = "%{usermodfmt} # %{password}\n".to_string();
        }
        if format_string.trim() == "mysql" {
            format_string = "%{mysqlfmt} -- %{password}\n".to_string();
        }
        if format_string.trim() == "pg" {
            format_string = "%{pgfmt} -- %{password}\n".to_string();
        }
        if format_string.trim() == "htauth" {
            format_string = "# %{password}\n%{htauthfmt}\n".to_string();
        }

        format_string = process_format_string(&mut format_string, c, &pw);

        print!("{}", format_string);
        return;
    }

    if donefmtopt {
        return;
    }

    if matches.opt_present("md5") {
        println!("{} {}", pw, md5_crypt::hash(&pw).unwrap());
        return;
    }
    if matches.opt_present("sha1") {
        println!("{} {}", pw, sha1_crypt::hash(&pw).unwrap());
        return;
    }
    if matches.opt_present("sha256") {
        println!("{} {}", pw, sha256_crypt::hash(&pw).unwrap());
        return;
    }
    if matches.opt_present("sha512") {
        println!("{} {}", pw, sha512_crypt::hash(&pw).unwrap());
        return;
    }
    if matches.opt_present("bcrypt") {
        println!("{} {}", pw, bcrypt::hash(&pw).unwrap());
        return;
    }
    if matches.opt_present("des") {
        println!("{} {}", pw, unix_crypt::hash(&pw).unwrap());
        return;
    }

    for j in 1..8 {
        let pw = (if c.password.is_some() {
            c.password.as_ref().unwrap().to_string()
        } else {
            (*prng_string(c)).to_string()
        })
        .to_string();
        print!("{}{}", pw, if j != 8 { " " } else { "" },);
        if c.word_list.is_some() {
            break;
        }
    }
    println!();
}

fn parse_totp_item(c: &mut Config, items: Vec<&str>) {
    match items[0].to_lowercase().as_str() {
        "key" | "totp" => {
            c.totp_key = Some(items[1].to_string());
        }
        "name" | "username" => {
            c.username = Some(items[1].to_string());
        }
        "step" => {
            c.totp_step = match items[1].parse::<u32>() {
                Ok(l) => Some(l.into()),
                Err(_) => {
                    eprintln!("cannot convert {} to number", items[1]);
                    std::process::exit(1);
                }
            }
        }
        "algo" => {
            c.totp_algo = match items[1].to_lowercase().as_str() {
                "sha1" => TotpAlgo::Sha1,
                "sha256" => TotpAlgo::Sha256,
                "sha512" => TotpAlgo::Sha512,
                _ => {
                    eprintln!("cannot convert {} to a TOTP algorithm", items[1]);
                    std::process::exit(1);
                }
            }
        }
        "digits" => {
            c.len = match items[1].parse::<u32>() {
                Ok(l) => Some(l),
                Err(_) => {
                    eprintln!("cannot convert {} to number", items[1]);
                    std::process::exit(1);
                }
            }
        }
        "seconds" => {
            c.totp_seconds = match items[1].parse::<u32>() {
                Ok(l) => Some(l.into()),
                Err(_) => {
                    eprintln!("cannot convert {} to number", items[1]);
                    std::process::exit(1);
                }
            }
        }
        _ => {
            eprintln!("key {} isn't valid", items[0]);
            std::process::exit(1);
        }
    }
}

pub fn main_loop(matches: &getopts::Matches, c: &Config) {
    for _ in 0..c.number {
        if c.totp_key.is_some() {
            // step=30,hash=sha1,secret=sock; ;
            let totp_arr: Vec<_> = c.totp_key.as_ref().unwrap().split(";").collect();

            for i in totp_arr {
                let i = i.trim();
                if i.is_empty() {
                    continue;
                }

                let c: &mut Config = &mut c.clone();
                c.totp_key = Some(i.to_string());

                let parts: Vec<_> = i.split(",").collect();
                for p in parts {
                    let p = p.trim();
                    if p.is_empty() {
                        continue;
                    }

                    let items: Vec<_> = p.splitn(2, "=").collect();
                    if items.len() < 2 {
                        continue;
                    }

                    parse_totp_item(c, items);
                }
                pw_loop(matches, c);
            }
            continue;
        }
        pw_loop(matches, c);
    }
}

pub fn sleep(millis: u64) {
    let duration = Duration::from_millis(millis);
    thread::sleep(duration);
}

pub fn urgent_colour(remaining: i32) -> String {
    if remaining < 6 {
        "\x1b[0;31m".to_string()
    } else {
        "\x1b[0;32m".to_string()
    }
}

pub fn normal_colour() -> String {
    "\x1b[0m".to_string()
}
