use chrono::{DateTime, Utc};
use getopts::Options;
use regex::Regex;
use ripcalc::*;
use serde_json::Value;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::io::prelude::*;
use std::io::{BufRead, BufReader};
use std::os::fd::AsFd;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::Mutex;
use std::{str, thread, time};

fn default_cdb_format(
    distance: usize,
    config: &RefCell<Config>,
    lookup_ip: &mut Ip,
    matches: &getopts::Matches,
) -> String {
    let mut ret_str = "".to_string();
    if let Some(row) = cdb_lookup(lookup_ip, config) {
        if !matches.opt_present("format") {
            let mut c: Vec<_> = row.keys().collect();
            c.sort();
            for f in c {
                ret_str.push_str(&format!(
                    "{f:>width$}: {v}\n",
                    width = distance,
                    v = row.get(f).unwrap()
                ));
            }
        }
    }
    ret_str.to_string()
}

fn default_mmasn_format(
    distance: usize,
    config: &RefCell<Config>,
    lookup_ip: &Ip,
    matches: &getopts::Matches,
) -> String {
    let mut ret_str = "".to_string();

    if !matches.opt_present("mmasn") {
        return "".to_string();
    }

    let map = geoip_asn(config, lookup_ip);

    if let Some(map) = map {
        let mut k: Vec<_> = map.keys().collect();
        k.sort();

        for k in k {
            ret_str.push_str(&format!(
                "{f:>width$}: {v}\n",
                width = distance,
                f = k,
                v = map.get(k).unwrap()
            ));
        }
    }

    ret_str.to_string()
}

fn default_mmcountry_format(
    distance: usize,
    config: &RefCell<Config>,
    lookup_ip: &Ip,
    matches: &getopts::Matches,
) -> String {
    let mut ret_str = "".to_string();

    if !matches.opt_present("mmcountry") {
        return "".to_string();
    }

    let map = geoip_country(config, lookup_ip);

    if let Some(map) = map {
        let mut k: Vec<_> = map.keys().collect();
        k.sort();

        for k in k {
            ret_str.push_str(&format!(
                "{f:>width$}: {v}\n",
                width = distance,
                f = k,
                v = map.get(k).unwrap()
            ));
        }
    }

    ret_str.to_string()
}

fn default_mmcity_format(
    distance: usize,
    config: &RefCell<Config>,
    lookup_ip: &Ip,
    matches: &getopts::Matches,
) -> String {
    let mut ret_str = "".to_string();

    if !matches.opt_present("mmcity") {
        return "".to_string();
    }

    let map = geoip_city(config, lookup_ip);

    if let Some(map) = map {
        let mut k: Vec<_> = map.keys().collect();
        k.sort();

        for k in k {
            ret_str.push_str(&format!(
                "{f:>width$}: {v}\n",
                width = distance,
                f = k,
                v = map.get(k).unwrap()
            ));
        }
    }

    ret_str.to_string()
}

fn default_abuseipdb_format(
    distance: usize,
    config: &RefCell<Config>,
    lookup_ip: &Ip,
    matches: &getopts::Matches,
) -> String {
    let mut ret_str = "".to_string();

    if !matches.opt_present("abuseipdb") {
        return "".to_string();
    }

    let key = config
        .borrow()
        .options
        .get("abuseipdb")
        .unwrap()
        .to_string();

    let map = get_abuseipdb_details(config, lookup_ip, &key);

    let mut k: Vec<_> = map.keys().collect();
    k.sort();

    for k in k {
        ret_str.push_str(&format!(
            "{f:>width$}: {v}\n",
            width = distance,
            f = k,
            v = map.get(k).unwrap()
        ));
    }
    ret_str.to_string()
}

fn print_details(
    format_detail: &FormatDetail,
    matches: &getopts::Matches,
    rows: &Option<HashMap<Ip, NetRow>>,
    used: Option<&HashMap<Addr, bool>>,
    config: &RefCell<Config>,
) {
    let mut networks: Option<u32> = None;
    let ip = format_detail.ip.unwrap();

    if matches.opt_present("networks") {
        let nets = matches.opt_str("networks").unwrap().trim().parse().unwrap();

        if nets < ip.cidr
            && !(config_option_true(&config.borrow(), "encapsulating".to_string())
                && config_option_true(&config.borrow(), "group".to_string()))
        {
            eprintln!("{} is bigger than the network mask {}", nets, ip.cidr);
            std::process::exit(1);
        }

        match ip.address {
            Addr::V4(_) => {
                if nets > 32 {
                    eprintln!("{} is too big", nets);
                    std::process::exit(1);
                }
            }
            Addr::V6(_) => {
                if nets > 128 {
                    eprintln!("{} is too big", nets);
                    std::process::exit(1);
                }
            }
        }
        networks = Some(nets);
    }

    let mut formatted = if matches.opt_present("f") {
        matches.opt_str("f").unwrap()
    } else {
        let mut network_size = "Network size: %t".to_string();
        let width = 35;
        if matches.opt_present("networks") {
            network_size = format!("Networks ({}): %N", networks.as_ref().unwrap()).to_string();
        }
        let mut def = match ip.address {
            Addr::V4(_) => format!("{ip:>width$}/{cidr}\n{broadcast:>width$}\n{network:>width$}\n{subnet:>width$}\n{wildcard:>width$}\n{network_size:>width$}\n", ip="IP is: %a", cidr="%c", broadcast="Broadcast is: %b", network="Network is: %n", subnet="Subnet is: %s", wildcard="Wildcard is: %w", network_size=network_size, width=width),
            Addr::V6(_) => format!("{ip:>widthn$}/{cidr}\n{expanded:>width$}\n{network:>width$}\n{last_host_address:>width$}\n{subnet:>width$}\n{network_size:>widthn$}\n", ip="IP is: %a", cidr="%c", expanded="Expanded: %xa", network="Network is: %xn", last_host_address="Last host address: %xb", subnet="Subnet is: %xs", network_size=network_size, width=width+1, widthn=width),
        };

        if matches.opt_present("cdb") {
            let distance = match ip.address {
                Addr::V4(_) => width - 4,
                Addr::V6(_) => width - 4,
            };

            def.push_str(&default_cdb_format(
                distance,
                config,
                &mut ip.clone(),
                matches,
            ));
        }

        if matches.opt_present("abuseipdb") {
            let distance = match ip.address {
                Addr::V4(_) => width - 4,
                Addr::V6(_) => width - 4,
            };

            def.push_str(&default_abuseipdb_format(
                distance,
                config,
                &ip.clone(),
                matches,
            ));
        }

        if matches.opt_present("mmcity") {
            let distance = match ip.address {
                Addr::V4(_) => width - 4,
                Addr::V6(_) => width - 4,
            };

            def.push_str(&default_mmcity_format(
                distance,
                config,
                &ip.clone(),
                matches,
            ));
        }

        if matches.opt_present("mmasn") {
            let distance = match ip.address {
                Addr::V4(_) => width - 4,
                Addr::V6(_) => width - 4,
            };

            def.push_str(&default_mmasn_format(
                distance,
                config,
                &ip.clone(),
                matches,
            ));
        }

        if matches.opt_present("mmcountry") {
            let distance = match ip.address {
                Addr::V4(_) => width - 4,
                Addr::V6(_) => width - 4,
            };

            def.push_str(&default_mmcountry_format(
                distance,
                config,
                &ip.clone(),
                matches,
            ));
        }

        def
    };

    if formatted == "cidr" {
        formatted = "%a/%c\n".to_string();
    }

    if formatted == "short" {
        formatted = "%a\n".to_string();
    }

    if matches.opt_present("divide") {
        let divide: u32 = match matches.opt_str("divide").unwrap().trim().parse() {
            Ok(x) => x,
            Err(x) => {
                eprintln!("Cannot convert {} to number", x);
                std::process::exit(1);
            }
        };

        for ip_copy in addresses(ip, used, Some(divide)) {
            if let Some(m) = format_details(
                &FormatDetail {
                    ip: Some(&ip_copy),
                    line: None,
                },
                formatted.to_string(),
                rows,
                networks,
                Some(matches),
                config,
            ) {
                print!("{}", m);
            }
        }
        return;
    }

    if matches.opt_present("list") {
        if matches.opt_present("noexpand") {
            if let Some(m) = format_details(
                format_detail,
                formatted,
                rows,
                networks,
                Some(matches),
                config,
            ) {
                print!("{}", m);
            }
            return;
        }

        for ip_copy in addresses(ip, used, None) {
            if let Some(m) = format_details(
                &FormatDetail {
                    ip: Some(&ip_copy),
                    line: None,
                },
                formatted.to_string(),
                rows,
                networks,
                Some(matches),
                config,
            ) {
                print!("{}", m);
            }
        }
        return;
    }

    if let Some(m) = format_details(
        format_detail,
        formatted,
        rows,
        networks,
        Some(matches),
        config,
    ) {
        print!("{}", m);
    }
}

fn banner() -> String {
    format!(
        "{} version {}",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION")
    )
}

fn print_version() {
    println!("{}", &banner());
}

fn base_parse(base: Option<&String>) -> Option<i32> {
    match base {
        Some(x) => i32::from_str(x).ok(),
        None => None,
    }
}

fn process_json(
    u: &Value,
    field_name: String,
    rows: &mut Option<HashMap<Ip, NetRow>>,
    input_base: Option<i32>,
    _reverse: &Reverse,
    config: &RefCell<Config>,
) {
    if let Value::Array(a) = &u[field_name] {
        for i in a {
            let mut hm: HashMap<String, String> = HashMap::new();
            let mut ip: Option<Ip> = None;

            for (key, value) in i.as_object().unwrap() {
                let key = key.to_string();
                let key = key.trim().strip_prefix('"').unwrap_or(&key);
                let key = key.trim().strip_suffix('"').unwrap_or(key);

                let value = value.to_string();
                let value = value.trim().strip_prefix('"').unwrap_or(&value);
                let value = value.trim().strip_suffix('"').unwrap_or(value);

                hm.insert(key.to_string(), value.to_string());
            }

            if let Some(cidr) = hm.get("ipv6_prefix") {
                ip = parse_address_mask(&cidr.to_string(), None, None, input_base, false, config);
            }

            if let Some(cidr) = hm.get("ip_prefix") {
                ip = parse_address_mask(&cidr.to_string(), None, None, input_base, false, config);
            }

            if let Some(ip) = &ip {
                if rows.is_none() {
                    *rows = Some(HashMap::new());
                }

                rows.as_mut()
                    .unwrap()
                    .insert(ip.clone(), NetRow { row: hm.clone() });
            }
        }
    }
}

fn process_csv(
    config: &RefCell<Config>,
    mut reader: csv::Reader<File>,
    field_name: String,
    rows: &mut Option<HashMap<Ip, NetRow>>,
    input_base: Option<i32>,
    reverse: &Reverse,
) {
    let headers = reader.headers().unwrap();
    let mut header_names: Vec<String> = vec![];
    let mut field_num: Option<usize> = None;
    for i in 0..headers.len() {
        if headers[i] == field_name {
            field_num = Some(i);
        }
        header_names.push(headers[i].to_string());
    }
    if field_num.is_none() {
        eprintln!("Cannot find csv field {}", field_name);
        std::process::exit(1);
    }

    let field_num = field_num.unwrap();

    for result in reader.records() {
        if result.is_err() {
            if !config_option_true(&config.borrow(), "quiet".to_string()) {
                eprintln!("{:?}", result.err());
            }
            continue;
        }
        let record = result.as_ref().unwrap();

        if record.get(field_num).is_some() {
            let rec = record.get(field_num).unwrap();

            let ip = parse_address_mask(
                rec,
                None,
                None,
                input_base,
                matches!(reverse, Reverse::Both | Reverse::Input),
                config,
            );

            if ip.is_none() {
                if !config_option_true(&config.borrow(), "quiet".to_string()) {
                    eprintln!("Could not parse {:?}", result.err());
                }
                continue;
            }

            let ip = ip.unwrap();

            let mut hm = HashMap::new();
            for i in 0..header_names.len() {
                hm.insert(header_names[i].to_string(), record[i].to_string());
            }

            if rows.is_none() {
                *rows = Some(HashMap::new());
            }

            rows.as_mut()
                .unwrap()
                .insert(ip.clone(), NetRow { row: hm.clone() });
        }
    }
}

fn process_encapsulated_input(
    inside: Option<bool>,
    ip_args: &[Ip],
    i: &Ip,
    network_size: u32,
    used: &mut HashMap<Ip, bool>,
    config: &RefCell<Config>,
) {
    if !inside_filter(inside, ip_args, i) {
        return;
    }
    let mut network_size = network_size;

    used.insert(i.clone(), true);

    increment_seen_count(&mut config.borrow_mut(), i.clone());

    if config_option_true(&config.borrow(), "group".to_string()) {
        match i.address {
            Addr::V4(_) => {
                if network_size > 32 {
                    network_size = 32;
                }
            }
            Addr::V6(_) => {
                if network_size > 128 {
                    network_size = 128;
                }
            }
        }
        let net_mask = network(&Ip {
            address: i.address.clone(),
            cidr: network_size,
        });
        let nu = &mut config.borrow_mut().net_used;

        if nu.get(&net_mask).is_none() {
            nu.insert(net_mask.clone(), HashMap::new());
        }

        let nm = nu.get_mut(&net_mask).unwrap();
        nm.insert(i.clone(), true);
    }
}

fn process_group_encapsulation(
    matches: &getopts::Matches,
    used: HashMap<Ip, bool>,
    rows: &Option<HashMap<Ip, NetRow>>,
    config: &RefCell<Config>,
) {
    if config_cidr_grouping(&config.borrow()) {
        let group4 = group_mask(
            &config.borrow(),
            &parse_address_mask("127.0.0.1", None, None, None, false, config).unwrap(),
        );
        let group6 = group_mask(
            &config.borrow(),
            &parse_address_mask("::1", None, None, None, false, config).unwrap(),
        );

        match smallest_group_network_limited(&used, group4, group6) {
            Some(mut x) => {
                x.sort_by(|a, b| a.partial_cmp(b).unwrap());
                for y in x {
                    {
                        let network_size = match y.address {
                            Addr::V4(_) => group4,
                            Addr::V6(_) => group6,
                        };

                        let net_mask = network(&Ip {
                            address: y.address.clone(),
                            cidr: network_size,
                        });

                        let cfg = &mut config.borrow_mut();
                        if !cfg.net_used.contains_key(&net_mask) {
                            cfg.net_used.insert(net_mask.clone(), HashMap::new());
                        }

                        let nm = cfg.net_used.get(&net_mask).unwrap();

                        cfg.used = Some(nm.clone());
                    }

                    print_details(
                        &FormatDetail {
                            ip: Some(&y),
                            line: None,
                        },
                        matches,
                        rows,
                        None,
                        config,
                    );
                }
            }
            None => {
                eprintln!("Could not find an encapsulating network, sorry");
                std::process::exit(1);
            }
        }
    } else {
        match smallest_group_network(&used) {
            Some(x) => {
                print_details(
                    &FormatDetail {
                        ip: Some(&x),
                        line: None,
                    },
                    matches,
                    rows,
                    None,
                    config,
                );
            }
            None => {
                eprintln!("Could not find an encapsulating network, sorry");
                std::process::exit(1);
            }
        }
    }
}

fn filter_pos(config: &RefCell<Config>) -> Option<u32> {
    if config_option_true(&config.borrow(), "filterany".to_string()) {
        return None;
    }

    let position: u32 = if config_option_true(&config.borrow(), "filter".to_string()) {
        match config
            .borrow()
            .options
            .get("filter")
            .unwrap()
            .trim()
            .parse::<u32>()
        {
            Ok(x) => {
                if x < 1 {
                    eprintln!("filternum {} should be 1 or more", x);
                    std::process::exit(1);
                }
                x - 1
            }
            Err(x) => {
                eprintln!("Cannot convert {} to number", x);
                std::process::exit(1);
            }
        }
    } else {
        0
    };

    Some(position)
}

fn process_input_filter(
    reader: &mut Box<dyn BufRead>,
    ip_args: &[Ip],
    inside: Option<bool>,
    config: &RefCell<Config>,
    matches: &getopts::Matches,
    rows: &Option<HashMap<Ip, NetRow>>,
) {
    let base = base_parse(config.borrow().options.get("base"));
    let position = filter_pos(config);

    for line in reader.lines() {
        match line {
            Ok(line) => {
                let found = line_filter(config, position, &line, base);

                if let Some(found) = found {
                    if !inside_filter(inside, ip_args, &found) {
                        continue;
                    }
                    if matches.opt_present("format") {
                        let n = FormatDetail::new(Some(&found), Some(&line));
                        print_details(&n, matches, rows, None, config);
                    } else {
                        println!("{}", line);
                    }
                }
            }
            Err(x) => {
                eprintln!("Cannot read input: {}", x);
                std::process::exit(1);
            }
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn process_input_file(
    path: &str,
    matches: &getopts::Matches,
    input_base: Option<i32>,
    reverse: &Reverse,
    ip_args: &[Ip],
    rows: &Option<HashMap<Ip, NetRow>>,
    inside: Option<bool>,
    config: &RefCell<Config>,
) {
    let mut reader: Box<dyn BufRead> = if path == "-" {
        Box::new(BufReader::new(std::io::stdin()))
    } else {
        let path = std::path::Path::new(&path);
        if !path.exists() {
            println!(
                "Could not open {} as it does not exist",
                path.to_string_lossy()
            );
            std::process::exit(1);
        }
        Box::new(BufReader::new(File::open(path).unwrap()))
    };

    if matches.opt_present("filter")
        || matches.opt_present("filterany")
        || matches.opt_present("filternum")
    {
        process_input_filter(&mut reader, ip_args, inside, config, matches, rows);

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

    if matches.opt_present("available") {
        let mut used: HashMap<Addr, bool> = HashMap::new();
        for a in find_ips(&mut reader, input_base, reverse, config) {
            for ip in a {
                used.insert(ip.address, true);
            }
        }

        for arg in ip_args {
            print_details(
                &FormatDetail {
                    ip: Some(arg),
                    line: None,
                },
                matches,
                rows,
                Some(&used),
                config,
            );
        }
        std::process::exit(0);
    }

    if config_option_true(&config.borrow(), "encapsulating".to_string()) {
        let mut used: HashMap<Ip, bool> = HashMap::new();
        let mut network_size: u32 = 0;

        for a in find_ips(&mut reader, input_base, reverse, config) {
            for i in a {
                if config_cidr_grouping(&config.borrow()) {
                    network_size = group_mask(&config.borrow(), &i);
                }

                process_encapsulated_input(inside, ip_args, &i, network_size, &mut used, config);
            }
        }

        config.borrow_mut().used = Some(used.clone());

        process_group_encapsulation(matches, used, rows, config);

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

    let mut found_match = false;

    for a in find_ips(&mut reader, input_base, reverse, config) {
        for ip in a {
            increment_seen_count(&mut config.borrow_mut(), ip.clone());
            if inside_filter(inside, ip_args, &ip) {
                found_match = true;
                print_details(
                    &FormatDetail {
                        ip: Some(&ip),
                        line: None,
                    },
                    matches,
                    rows,
                    None,
                    config,
                );
            }
        }
    }

    if !found_match && inside.is_some() {
        std::process::exit(1);
    }
}

fn wait_stdin(matches: &getopts::Matches) -> bool {
    if matches.opt_present("available") {
        return true;
    }
    if matches.opt_present("list")
        || matches.opt_present("inside")
        || matches.opt_present("outside")
        || matches.opt_present("filter")
        || matches.opt_present("filterany")
        || matches.opt_present("filternum")
    {
        return true;
    }
    if matches.free.is_empty() {
        return true;
    }
    false
}

fn generated_date(cdb: &mut cdb::CDBWriter, dt: DateTime<Utc>) {
    let _ = cdb.add(
        "generated_on".as_bytes(),
        dt.format("%Y-%m-%d %H:%M:%S").to_string().as_bytes(),
    );
    let _ = cdb.add("version".as_bytes(), "1".as_bytes());
}

fn make_cdb(path: String, config: &RefCell<Config>) {
    let reader = BufReader::new(std::io::stdin());
    let tmp_file = format!("{}.tmp", path);
    let cdb = cdb::CDBWriter::create(&tmp_file);

    if let Err(msg) = cdb {
        eprintln!("Cannot create {}: {}", tmp_file, msg);
        std::process::exit(1);
    }

    let mut cdb = cdb.unwrap();

    for line in reader.lines() {
        match line {
            Ok(_) => {
                let line = line.unwrap();
                let mut parts: Vec<&str> = line.split(',').collect();

                if parts.len() < 2 {
                    eprintln!("Not sufficient parts in line: {}", line);
                    continue;
                }

                for j in &mut parts {
                    *j = j.trim();
                }

                let key = parts[0];
                let value = parts[1..].join("\0");

                if let Some(key) = parse_address_mask(key, None, None, None, false, config) {
                    let key = format!("{}/{}", network(&key), key.cidr);
                    let _ = cdb.add(key.as_bytes(), value.as_bytes());
                    continue;
                }

                // treat other input (trimmed) verbatim
                let _ = cdb.add(key.as_bytes(), value.as_bytes());
            }
            Err(x) => {
                eprintln!("Cannot read: {}", x);
                std::process::exit(1);
            }
        }
    }

    if cdb.finish().is_err() {
        eprintln!("Cannot finish writing: {}", tmp_file);
        std::process::exit(1);
    }

    if fs::rename(&tmp_file, &path).is_err() {
        eprintln!("Cannot rename {} to {}", tmp_file, path);
        std::process::exit(1);
    }
}

fn make_thyme_cdb(location: String, config: &RefCell<Config>) {
    fn process_raw(
        data: String,
        db: &mut HashMap<String, String>,
        asn_list: &mut HashMap<String, Vec<String>>,
    ) {
        let r = Regex::new(r#"^\s*([\d.]+\/\d+)\s+(\d+)\s*$"#).unwrap();

        for l in data.split("\n") {
            let c = r.captures(l);
            if c.is_none() {
                continue;
            }
            let c = c.unwrap();

            let ip = c[1].to_string();
            let prefix = c[2].to_string();

            db.insert(ip.clone(), prefix.clone());
            if asn_list.get(&prefix).is_none() {
                asn_list.insert(prefix.clone(), vec![]);
            }

            asn_list.get_mut(&prefix).unwrap().push(ip.clone());
        }
    }

    fn process_ipv6(
        data: String,
        db: &mut HashMap<String, String>,
        asn_list: &mut HashMap<String, Vec<String>>,
    ) {
        let r = Regex::new(r#"\s*(\S+)\s+(\S+)"#).unwrap();

        for l in data.split("\n") {
            let c = r.captures(l);
            if c.is_none() {
                continue;
            }
            let c = c.unwrap();

            let ip = c[1].to_string();
            let prefix = c[2].to_string();

            db.insert(ip.clone(), prefix.clone());
            if asn_list.get(&prefix).is_none() {
                asn_list.insert(prefix.clone(), vec![]);
            }

            asn_list.get_mut(&prefix).unwrap().push(ip.clone());
        }
    }

    fn process_asn(
        cdb: &mut cdb::CDBWriter,
        lines: String,
        asn: &mut HashMap<String, String>,
        data: &HashMap<String, String>,
        asn_list: &mut HashMap<String, Vec<String>>,
    ) {
        let s = Regex::new(r#"^\s*(?<num>[0-9]+)\s+(?<desc>.*?)(,\s+)?(?<cc>\S+)?\s*$"#).unwrap();

        for l in lines.split("\n") {
            let c = s.captures(l);
            if c.is_none() {
                continue;
            }
            let c = c.unwrap();

            let num = &c["num"];
            let desc = &c["desc"];
            let cc = if c.name("cc").is_some() {
                &c["cc"]
            } else {
                "XX"
            };
            asn.insert(
                c[1].to_string(),
                format!("ASNDESC={desc}\0ASNCC={cc}\0ASN={num}"),
            );
        }

        for d in data.keys() {
            if asn.get(data.get(d).unwrap()).is_some() {
                if let Err(e) = cdb.add(
                    d.as_bytes(),
                    asn.get(data.get(d).unwrap()).unwrap().as_bytes(),
                ) {
                    eprintln!("Could not add {}: {}", d, e);
                    std::process::exit(1);
                }
            }
        }

        for k in asn_list.keys() {
            if let Err(e) = cdb.add(k.as_bytes(), asn_list.get(k).unwrap().join("\0").as_bytes()) {
                eprintln!("Could not add {}: {}", k, e);
                std::process::exit(1);
            }
        }
    }

    fn get_or_exit(url: &str, client: &reqwest::blocking::Client) -> String {
        if !url.starts_with("http") {
            let path = std::path::Path::new(&url);
            if !path.exists() {
                println!(
                    "Could not open {} as it does not exist",
                    path.to_string_lossy()
                );
                std::process::exit(1);
            }
            let f = File::open(path);
            if let Err(error) = f {
                eprintln!("cannot open {}: {}", url, error);
                std::process::exit(1);
            };
            let mut f = f.unwrap();
            let mut content = String::new();
            if let Err(error) = f.read_to_string(&mut content) {
                eprintln!("cannot read {}: {}", url, error);
                std::process::exit(1);
            };
            return content;
        }

        let response = client.get(url).send();
        if response.as_ref().is_err() {
            eprintln!("Could not communicate with {}", url);
            std::process::exit(1);
        }
        if let Err(error) = response.as_ref().unwrap().error_for_status_ref() {
            eprintln!("Could not get a text response from {}: {}", url, error);
            std::process::exit(1);
        }

        let response = response.unwrap();
        let t = response.text();

        match t {
            Ok(x) => x.to_string(),
            Err(ref error) => {
                eprintln!("Could not get a text response from {}: {}", url, error);
                std::process::exit(1);
            }
        }
    }

    let client = ua_client().expect("Cannot make user-agent");
    let tmp_file = format!("{}.tmp", &location);
    let cdb = cdb::CDBWriter::create(&tmp_file);

    if let Err(msg) = cdb {
        eprintln!("Cannot create {}: {}", &tmp_file, msg);
        std::process::exit(1);
    }
    let mut cdb = cdb.unwrap();

    let mut asn: HashMap<String, String> = HashMap::new();
    let mut asn_list: HashMap<String, Vec<String>> = HashMap::new();
    let mut data: HashMap<String, String> = HashMap::new();

    let data_raw = get_or_exit(
        config.borrow().options.get("data-raw-table").unwrap(),
        &client,
    );
    process_raw(data_raw, &mut data, &mut asn_list);

    let ipv6_raw = get_or_exit(
        config.borrow().options.get("ipv6-raw-table").unwrap(),
        &client,
    );
    process_ipv6(ipv6_raw, &mut data, &mut asn_list);

    let data_used = get_or_exit(
        config.borrow().options.get("data-used-autnums").unwrap(),
        &client,
    );
    process_asn(&mut cdb, data_used, &mut asn, &data, &mut asn_list);

    generated_date(&mut cdb, Utc::now());

    if let Err(error) = cdb.finish() {
        eprintln!("Cannot write {}: {}", tmp_file, error);
        std::process::exit(1);
    }

    if let Err(error) = fs::rename(&tmp_file, &location) {
        eprintln!("Cannot rename {} to {}: {}", &tmp_file, &location, &error);
        std::process::exit(1);
    }
}

fn set_opts(opts: &mut Options) {
    opts.parsing_style(getopts::ParsingStyle::FloatingFrees);
    opts.optflag("4", "ipv4", "treat inputs as ipv4 address");
    opts.optflag("6", "ipv6", "treat inputs as ipv6 address");

    opts.optflag("a", "available", "display unused addresses");
    opts.optopt("", "abuseipdb", "abuseipdb API", "KEY");
    opts.optflag(
        "",
        "allowemptyrow",
        "when no matching cdb/csv network, use empty fields",
    );
    opts.optopt("b", "base", "ipv4 base format, default to oct", "INTEGER");
    opts.optopt("", "cdb", "cdb reference file", "PATH");
    opts.optflag("", "countseen", "count times an ip is seen");
    opts.optopt("c", "csv", "csv reference file", "PATH");
    opts.optopt("d", "divide", "divide network into chunks", "CIDR");
    opts.optflag("", "noexpand", "do not expand networks in list");

    opts.optflag(
        "e",
        "encapsulating",
        "display encapsulating network from arguments or lookup list",
    );

    opts.optopt("f", "format", "format output\n'cidr' expands to %a/%c\\n\n'short' expands to %a\\n\nSee manual for more options", "STRING");
    opts.optflag("", "filter", "print STDIN line if field parses as an IP and matches, defaults to 1, use filternum to adjust");
    opts.optflag(
        "",
        "filterany",
        "print STDIN line if any IP on STDIN matches",
    );
    opts.optopt(
        "",
        "filternum",
        "change position NUMBER for IP filter, enables --filter",
        "NUMBER",
    );
    opts.optopt(
        "",
        "group",
        "maximum network group size for encapsulation",
        "CIDR",
    );
    opts.optopt(
        "",
        "group4",
        "v4 specific group, overrides group in top mode",
        "CIDR",
    );
    opts.optopt(
        "",
        "group6",
        "v6 specific group, overrides group in top mode",
        "CIDR",
    );

    opts.optflag("h", "help", "display help");

    opts.optopt("i", "field", "csv/db/json field", "FIELD");
    opts.optflag("l", "list", "list all addresses in network");
    opts.optflag(
        "",
        "outside",
        "display when extremities are outside network",
    );
    opts.optflag("", "inside", "display when extremities are inside network");
    opts.optopt("", "makecdb", "build a cdb file from STDIN", "PATH");
    opts.optopt(
        "",
        "makethymecdb",
        "download and build a cdb file, defaults to thyme.apnic.net data",
        "PATH",
    );
    opts.optopt("", "data-raw-table", "URL/path of raw data", "URL or PATH");
    opts.optopt("", "ipv6-raw-table", "URL/path of ipv6 data", "URL or PATH");
    opts.optopt(
        "",
        "data-used-autnums",
        "URL/path of ASN data",
        "URL or PATH",
    );

    opts.optflag("", "top", "show IP frequency like top");
    opts.optopt("", "delay", "top update delay in seconds", "SECONDS");
    opts.optopt("", "iterations", "top iterations ", "NUMBER");
    opts.optflag("", "noclear", "don't print clear screen codes");

    opts.optopt(
        "",
        "mmlang",
        "the lang to use in results, defaults to en",
        "LANG",
    );
    opts.optopt("", "mmasn", "path to maxmind geoip asn file", "PATH");
    opts.optopt("", "mmcity", "path to maxmind geoip city file", "PATH");
    opts.optopt(
        "",
        "mmcountry",
        "path to maxmind geoip country file",
        "PATH",
    );

    opts.optopt("m", "mask", "cidr mask", "CIDR");
    opts.optopt(
        "n",
        "networks",
        "instead of hosts, display number of subnets of this size",
        "CIDR",
    );

    opts.optopt("", "json", "json prefix file", "PATH");
    opts.optflag("q", "quiet", "don't report errors");

    opts.optopt(
        "r",
        "reverse",
        "(none, inputs, sources or both) v4 octets, v6 hex",
        "",
    );
    opts.optopt("s", "file", "lookup addresses from, - for stdin", "PATH");

    opts.optflag("v", "version", "print version");
}

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

fn read_loop(
    config: &RefCell<Config>,
    matches: &getopts::Matches,
    inside: Option<bool>,
    ip_args: &[Ip],
) {
    let ip_args: Vec<_> = ip_args.to_vec();

    fn cidr_optional_mask(i: &Ip) -> String {
        format!(
            "{}{}",
            i,
            match i.address {
                Addr::V4(_) => {
                    if i.cidr != 32 {
                        format!("/{}", i.cidr)
                    } else {
                        String::new()
                    }
                }
                Addr::V6(_) => {
                    if i.cidr != 128 {
                        format!("/{}", i.cidr)
                    } else {
                        String::new()
                    }
                }
            }
        )
    }

    let mut reader = BufReader::new(std::io::stdin());
    config
        .borrow_mut()
        .options
        .insert("countseen".to_string(), "true".to_string());
    config.borrow_mut().count = Some(HashMap::new());
    let count: Arc<Mutex<Config>> = Mutex::new(config.borrow().clone()).into();

    let count_lock = count.clone();

    thread::spawn(move || {
        let config = RefCell::new(count_lock.lock().unwrap().clone());
        let base = base_parse(config.borrow().options.get("base"));
        let position = filter_pos(&config);

        loop {
            let mut m = String::new();
            let _ = reader.read_line(&mut m);
            let found = line_filter(&config, position, &m, base);
            if let Some(ip) = found {
                if !inside_filter(inside, &ip_args, &ip) {
                    continue;
                }

                let mut counter_lock = count_lock.lock().unwrap();
                increment_seen_count(&mut counter_lock, ip);
            }
        }
    });

    let delay = {
        let cfg = config.borrow();
        let delay = cfg
            .options
            .get("delay")
            .map_or("1", |v| v)
            .parse::<u64>()
            .unwrap_or(1);
        delay * 1000
    };

    let iterations = {
        if config_option_true(&config.borrow(), "iterations".to_string()) {
            let cfg = config.borrow();
            let iterations = cfg.options.get("iterations").unwrap();

            let iterations = iterations
                .parse::<u32>()
                .unwrap_or_else(|_| panic!("Cannot convert {} to number", iterations));

            Some(iterations)
        } else {
            None
        }
    };

    let mut loops = 0;
    let clear_screen = !config_option_true(&config.borrow(), "noclear".to_string());
    loop {
        if clear_screen {
            print!("\x1b\x5b\x48\x1b\x5b\x32\x4a");
        }
        let _ = std::io::stdout().flush();
        let hm;
        {
            hm = count.lock().unwrap().clone();
            count.lock().unwrap().count = Some(HashMap::new());
        }

        if let Some(count) = hm.count {
            let mut v: Vec<_> = count.iter().collect();
            v.sort_by(|x, y| y.1.cmp(x.1));
            let mut c = 0;
            let mut total = 0;
            for i in &v {
                total += i.1;
            }

            for i in v {
                let pct = (10.0 / total as f64) * (*i.1 as f64);
                let cidr = cidr_optional_mask(i.0);

                let fmt_str = match matches.opt_str("format") {
                    Some(x) => x,
                    None => {
                        let mut parts = vec![];
                        if matches.opt_str("cdb").is_some() {
                            parts.push("%{ASNCC}".to_string());
                        }

                        if matches.opt_str("abuseipdb").is_some() {
                            parts.push("%{abuseipdb_abuseConfidenceScore}".to_string());
                        }

                        if matches.opt_str("mmcity").is_some() {
                            parts.push("%{mmcity_code}".to_string());
                            parts.push("%{mmcity_country}".to_string());
                        }

                        if matches.opt_str("mmasn").is_some() {
                            parts.push("%{mmasn_autonomous_system_organization}".to_string());
                        }

                        if matches.opt_str("mmcountry").is_some() {
                            parts.push("%{mmcountry_registered_country}".to_string());
                        }

                        if matches.opt_str("cdb").is_some() {
                            parts.push("%{ASNDESC} ".to_string());
                        }

                        parts.join(" ")
                    }
                };

                if let Some(m) = format_details(
                    &FormatDetail {
                        ip: Some(&i.0.clone()),
                        line: None,
                    },
                    fmt_str.clone(),
                    &None,
                    None,
                    Some(matches),
                    config,
                ) {
                    let asn_col = 47;
                    let ip_len = 15;
                    let m = &m[..if m.len() > asn_col { asn_col } else { m.len() }];
                    let ipcol = if !fmt_str.is_empty() {
                        let asn_col = if cidr.len() > ip_len {
                            asn_col + ip_len - cidr.len()
                        } else {
                            asn_col
                        };

                        let m = &m[..if m.len() > asn_col { asn_col } else { m.len() }];
                        format!("{:asn_col$} {:>ip_len$}", m, cidr)
                    } else {
                        format!("{:>width$}", cidr, width = asn_col + ip_len + 1)
                    };

                    let s = format!(
                        "{:60} {:4} {empty:#<pct$}",
                        ipcol,
                        i.1,
                        empty = "",
                        pct = pct as usize
                    );

                    println!("{}", s);
                    c += 1;
                    if c > 20 {
                        break;
                    }
                }
            }
            if c > 0 {
                println!();
            }
        }
        if let Some(iterations) = iterations {
            if loops >= iterations {
                return;
            }
            loops += 1;
        }
        sleep(delay);
    }
}

fn set_group_options(config: &RefCell<Config>, matches: &getopts::Matches) {
    if matches.opt_present("group") {
        let n: u32 = match matches.opt_str("group").unwrap().trim().parse() {
            Ok(x) => {
                if x > 128 {
                    eprintln!("Cannot use a group greater than 128");
                    std::process::exit(1);
                }
                x
            }
            Err(x) => {
                eprintln!("Cannot convert {} to number", x);
                std::process::exit(1);
            }
        };
        config
            .borrow_mut()
            .options
            .insert("group".to_string(), n.to_string());
    }

    if matches.opt_present("group4") {
        let n: u32 = match matches.opt_str("group4").unwrap().trim().parse() {
            Ok(x) => {
                if x > 32 {
                    eprintln!("Cannot use a group greater than 32");
                    std::process::exit(1);
                }
                x
            }
            Err(x) => {
                eprintln!("Cannot convert {} to number", x);
                std::process::exit(1);
            }
        };
        config
            .borrow_mut()
            .options
            .insert("group4".to_string(), n.to_string());
    }

    if matches.opt_present("group6") {
        let n: u32 = match matches.opt_str("group6").unwrap().trim().parse() {
            Ok(x) => {
                if x > 128 {
                    eprintln!("Cannot use a group greater than 128");
                    std::process::exit(1);
                }
                x
            }
            Err(x) => {
                eprintln!("Cannot convert {} to number", x);
                std::process::exit(1);
            }
        };
        config
            .borrow_mut()
            .options
            .insert("group6".to_string(), n.to_string());
    }
}

fn main() {
    let mut opts = Options::new();
    let mut rows: Option<HashMap<Ip, NetRow>> = None;
    let input_ip: Option<Addr> = None;
    let mut input_mask: Option<u32> = None;
    let mut input_base: Option<i32> = None;
    let mut reverse = Reverse::None;
    let mut inside: Option<bool> = None;
    let args: Vec<String> = std::env::args().collect();
    let mut ip_args: Vec<Ip> = vec![];
    let config = RefCell::new(Config::new());

    set_opts(&mut opts);

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

    if matches.opt_present("h") {
        println!("{}", opts.usage(&banner()));

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

    if matches.opt_present("version") {
        print_version();
        std::process::exit(0);
    }

    if matches.opt_present("inside") {
        inside = Some(true);
    }

    if matches.opt_present("outside") {
        if inside.is_some() {
            println!("Cannot combine --inside and --outside");
            std::process::exit(1);
        }
        inside = Some(false);
    }

    if matches.opt_present("quiet") {
        config
            .borrow_mut()
            .options
            .insert("quiet".to_string(), "true".to_string());
    }

    set_group_options(&config, &matches);

    if matches.opt_present("networks") {
        let _: u32 = match matches.opt_str("networks").unwrap().trim().parse() {
            Ok(x) => x,
            Err(x) => {
                eprintln!("Cannot convert {} to number", x);
                std::process::exit(1);
            }
        };
    }

    if matches.opt_present("reverse") {
        match matches.opt_str("reverse").unwrap().as_str() {
            "inputs" => {
                reverse = Reverse::Input;
            }
            "sources" => {
                reverse = Reverse::Source;
            }
            "both" => {
                reverse = Reverse::Both;
            }
            _ => {
                println!("reverse is not one of inputs, sources or both");
                std::process::exit(1);
            }
        }
    }

    if matches.opt_present("base") {
        let param = matches.opt_str("base");
        input_base = base_parse(param.as_ref());
        if let Some(input) = input_base {
            config
                .borrow_mut()
                .options
                .insert("base".to_string(), input.to_string());
        } else {
            println!("Cannot convert to an integer base: {}", param.unwrap());
            std::process::exit(1);
        }
    }

    if matches.opt_present("csv") {
        let path = matches.opt_str("csv").unwrap();
        let reader = match csv::Reader::from_path(&path) {
            Ok(r) => r,
            Err(x) => {
                eprintln!("Cannot open {}: {}", &path, x);
                std::process::exit(1);
            }
        };
        let field_name = if matches.opt_present("field") {
            matches.opt_str("field").unwrap()
        } else {
            "network".to_string()
        };

        process_csv(&config, reader, field_name, &mut rows, input_base, &reverse);
    }

    if matches.opt_present("json") {
        if let Some(path) = matches.opt_str("json") {
            let file = File::open(&path).unwrap_or_else(|_| panic!("Cannot open {}", path));
            let reader = BufReader::new(file);

            let field_name = if matches.opt_present("field") {
                matches.opt_str("field").unwrap()
            } else {
                "prefixes".to_string()
            };

            let u: Value =
                serde_json::from_reader(reader).unwrap_or_else(|_| panic!("Cannot parse {}", path));
            process_json(&u, field_name, &mut rows, input_base, &reverse, &config);
        }
    }

    if let Some(path) = matches.opt_str("cdb") {
        config
            .borrow_mut()
            .options
            .insert("cdb_path".to_string(), path);
    }

    if let Some(lang) = matches.opt_str("mmlang") {
        config
            .borrow_mut()
            .options
            .insert("mmlang".to_string(), lang.clone());
    }

    if let Some(path) = matches.opt_str("mmcity") {
        config
            .borrow_mut()
            .options
            .insert("mmcity".to_string(), path.clone());
    }

    if let Some(path) = matches.opt_str("mmcountry") {
        config
            .borrow_mut()
            .options
            .insert("mmcountry".to_string(), path.clone());
    }

    if let Some(path) = matches.opt_str("mmasn") {
        config
            .borrow_mut()
            .options
            .insert("mmasn".to_string(), path.clone());
    }

    if matches.opt_present("countseen") {
        config
            .borrow_mut()
            .options
            .insert("countseen".to_string(), "true".to_string());
        config.borrow_mut().count = Some(HashMap::new());
        config
            .borrow_mut()
            .options
            .insert("encapsulating".to_string(), "true".to_string());
        if !config_option_true(&config.borrow(), "group".to_string()) {
            config
                .borrow_mut()
                .options
                .insert("group".to_string(), "128".to_string());
        }
    }

    if let Some(v) = matches.opt_str("mask") {
        input_mask = parse_mask(&v);
    }

    if let Some(v) = matches.opt_str("makethymecdb") {
        config.borrow_mut().options.insert(
            "data-raw-table".to_string(),
            matches
                .opt_str("data-raw-table")
                .unwrap_or("https://thyme.apnic.net/.combined/data-raw-table".to_string()),
        );
        config.borrow_mut().options.insert(
            "ipv6-raw-table".to_string(),
            matches
                .opt_str("ipv6-raw-table")
                .unwrap_or("https://thyme.apnic.net/.combined/ipv6-raw-table".to_string()),
        );
        config.borrow_mut().options.insert(
            "data-used-autnums".to_string(),
            matches
                .opt_str("data-used-autnums")
                .unwrap_or("https://thyme.apnic.net/.combined/data-used-autnums".to_string()),
        );

        make_thyme_cdb(v, &config);
        std::process::exit(0);
    }

    if let Some(v) = matches.opt_str("makecdb") {
        make_cdb(v, &config);
        std::process::exit(0);
    }

    if matches.opt_present("ipv4") {
        config.borrow_mut().input_family = Some(InputFamily::IPv4);
    }

    if matches.opt_present("ipv6") {
        config.borrow_mut().input_family = Some(InputFamily::IPv6);
    }

    if matches.opt_present("filter") {
        config
            .borrow_mut()
            .options
            .insert("filter".to_string(), String::from("1"));
    }

    if matches.opt_present("filterany") {
        config
            .borrow_mut()
            .options
            .insert("filterany".to_string(), "true".to_string());
    }

    if matches.opt_present("encapsulating") {
        config
            .borrow_mut()
            .options
            .insert("encapsulating".to_string(), "true".to_string());
    }

    if matches.opt_present("abuseipdb") {
        config.borrow_mut().options.insert(
            "abuseipdb".to_string(),
            matches.opt_str("abuseipdb").unwrap(),
        );
    }

    if matches.opt_present("filternum") {
        let num: u32 = match matches.opt_str("filternum").unwrap().trim().parse() {
            Ok(x) => x,
            Err(x) => {
                eprintln!("Cannot convert {} to number", x);
                std::process::exit(1);
            }
        };

        config
            .borrow_mut()
            .options
            .insert("filter".to_string(), num.to_string());
    }

    if input_mask.is_none() && matches.free.is_empty() {
        input_mask = Some(32);

        if let Some(Addr::V6(_)) = input_ip {
            input_mask = Some(128);
        }
    }

    if let Some(input_ip) = input_ip {
        ip_args.push(Ip {
            address: input_ip,
            cidr: input_mask.unwrap(),
        });
    }

    let free_arg = matches.free.clone();
    if !free_arg.is_empty() {
        for arg in &free_arg {
            let ip = parse_address_mask(
                arg,
                None,
                None,
                input_base,
                matches!(reverse, Reverse::Both | Reverse::Input),
                &config,
            );

            if let Some(ip) = ip {
                ip_args.push(ip);
            }
        }
    }

    if matches.opt_present("delay") {
        config.borrow_mut().options.insert(
            "delay".to_string(),
            matches.opt_str("delay").unwrap().to_string(),
        );
    }

    if matches.opt_present("noclear") {
        config
            .borrow_mut()
            .options
            .insert("noclear".to_string(), "true".to_string());
    }

    if let Some(iterations) = matches.opt_str("iterations") {
        match matches.opt_str("iterations").unwrap().trim().parse::<u32>() {
            Ok(_) => {
                config
                    .borrow_mut()
                    .options
                    .insert("iterations".to_string(), iterations);
            }
            Err(x) => {
                eprintln!("Cannot parse {} as a number: {}", iterations, x);
                std::process::exit(1);
            }
        }
    }

    if matches.opt_present("top") {
        config
            .borrow_mut()
            .options
            .insert("top".to_string(), "true".to_string());

        read_loop(&config, &matches, inside, &ip_args);
        std::process::exit(0);
    }

    let stdin_ready = fd_ready(std::io::stdin().as_fd());
    if (stdin_ready && wait_stdin(&matches)) || matches.opt_str("file").is_some() {
        let path = if stdin_ready {
            "-".to_string()
        } else {
            matches.opt_str("file").unwrap()
        };
        process_input_file(
            &path, &matches, input_base, &reverse, &ip_args, &rows, inside, &config,
        );

        if ip_args.clone().is_empty() || inside.is_some() {
            std::process::exit(0);
        }
    }

    let mut used: HashMap<Ip, bool> = HashMap::new();

    for arg in &ip_args {
        match arg.address {
            Addr::V4(_) => {
                if arg.cidr > 32 {
                    eprintln!("V4 mask cannot be greater than 32");
                    std::process::exit(1);
                }
            }
            Addr::V6(_) => {
                if arg.cidr > 128 {
                    eprintln!("V6 mask cannot be greater than 128");
                    std::process::exit(1);
                }
            }
        }

        if config_option_true(&config.borrow(), "encapsulating".to_string()) {
            let mut network_size: u32 = 0;

            if let Some(group) = config.borrow().options.get("group") {
                network_size = group.trim().parse().unwrap();
            }

            process_encapsulated_input(inside, &ip_args, arg, network_size, &mut used, &config);
            used.insert(arg.clone(), true);
            continue;
        }
        print_details(
            &FormatDetail {
                ip: Some(arg),
                line: None,
            },
            &matches,
            &rows,
            None,
            &config,
        );
    }

    config.borrow_mut().used = Some(used.clone());

    if config_option_true(&config.borrow(), "encapsulating".to_string()) {
        process_group_encapsulation(&matches, used, &rows, &config);
        std::process::exit(0);
    }

    if ip_args.is_empty() {
        println!("{}", opts.usage("ripcalc"));
        eprintln!("Need to provide v4 or v6 address.");
        std::process::exit(1);
    }

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