mod cli;
mod config;
mod lookup;
mod output;
mod tlds;
mod tui;
mod types;

use clap::Parser;
use cli::{print_fullhelp, print_help, Args};
use config::{parse_filter_file, resolve_paths, Config};
use tlds::{apply_top_tlds, get_tlds_or_default, whois_overrides};
use types::{DomainResult, ErrorKind};

#[derive(Debug)]
struct AggregatedResult {
    domain_idx: usize,
    tld_idx: usize,
    result: DomainResult,
}

#[tokio::main]
async fn main() {
    let args = Args::parse();

    // handle help flags
    if args.help {
        print_help();
        return;
    }
    if args.fullhelp {
        print_fullhelp();
        return;
    }

    // resolve .hoardom directory structure
    let paths = resolve_paths(args.env_path.as_ref());
    let mut config = Config::load_with_backup(&paths.config_file);

    if !paths.can_save {
        eprintln!("Warning: Unpriviliged bitch detected! Cant write to {}", paths.config_file.display());
    }

    // handle -r refresh cache flag
    if args.refresh_cache {
        if !paths.caching_enabled {
            eprintln!("Caching aint gon work without a writable location");
            return;
        }
        let cache_file = paths.cache_file("rdap_bootstrap.json");
        match lookup::refresh_cache(&cache_file, args.verbose).await {
            Ok(()) => {
                config.mark_cache_updated();
                if paths.can_save {
                    let _ = config.save(&paths.config_file);
                }
            }
            Err(e) => eprintln!("Error refreshing cache: {}", e),
        }
        return;
    }

    // check if cache is stale and auto update if needed
    let cache_file = if paths.caching_enabled {
        Some(paths.cache_file("rdap_bootstrap.json"))
    } else {
        None
    };

    let force_refresh = if let Some(ref cf) = cache_file {
        let (is_outdated, should_auto) = config.check_cache_status();
        if is_outdated && !should_auto {
            eprintln!("Warning: RDAP cache is outdated. Run `hoardom -r` to refresh.");
        }
        should_auto || !cf.exists()
    } else {
        false
    };

    // import custom sexy list if given
    if let Some(list_path) = &args.import_list {
        match parse_filter_file(list_path) {
            Ok(filter) => {
                config.import_filter(filter);
                if paths.can_save {
                    let _ = config.save(&paths.config_file);
                }
            }
            Err(e) => {
                eprintln!("Error importing list: {}", e);
                return;
            }
        }
    }

    // whois server overrides that are baked into Lists.toml (like this "tld:server" for naighty tlds)
    let overrides = whois_overrides();

    let noretry: Vec<ErrorKind> = config
        .settings
        .noretry
        .iter()
        .filter_map(|s| ErrorKind::from_config_str(s))
        .collect();

    // the sigma mode
    if args.is_tui() {
        if let Err(e) = tui::run_tui(
            &args,
            &config,
            paths.clone(),
            cache_file.clone(),
            force_refresh,
            overrides.clone(),
            noretry.clone(),
        )
        .await
        {
            eprintln!("TUI error: {}", e);
        }
        // save cache timestamp to know how fresh we are 
        if force_refresh && paths.can_save {
            config.mark_cache_updated();
            let _ = config.save(&paths.config_file);
        }
        return;
    }

    // CLI needs at least one domain unless autosearch was given user is stupid show small help
    if args.domains.is_empty() {
        if let Some(file_path) = &args.autosearch {
            run_autosearch(
                &args,
                file_path,
                cache_file.as_deref(),
                force_refresh,
                overrides,
                &noretry,
            )
            .await;
            if force_refresh && paths.can_save {
                config.mark_cache_updated();
                let _ = config.save(&paths.config_file);
            }
            return;
        }
        print_help();
        return;
    }

    let base_tlds = build_base_tlds(&args);
    let total_lookups = estimate_total_lookups(&args.domains, &base_tlds);
    let mut completed_lookups = 0usize;
    let mut refresh_remaining = force_refresh;
    let mut aggregated_results = Vec::new();

    for (domain_idx, raw_domain) in args.domains.iter().enumerate() {
        let (search_name, specific_tld) = parse_domain_input(raw_domain);
        let effective_tlds = build_effective_tlds(&base_tlds, specific_tld.as_deref());

        if effective_tlds.is_empty() {
            eprintln!("No TLDs to search. Check your filter settings.");
            return;
        }

        let results = lookup::lookup_all(
            &search_name,
            &effective_tlds,
            args.effective_delay(),
            args.effective_retry(),
            args.verbose,
            cache_file.as_deref(),
            refresh_remaining,
            args.effective_jobs(),
            overrides,
            &noretry,
            |current, _total| {
                output::print_progress(completed_lookups + current, total_lookups.max(1));
            },
        )
        .await;

        refresh_remaining = false;
        completed_lookups += effective_tlds.len();

        for result in results {
            let tld_idx = effective_tlds
                .iter()
                .position(|tld| *tld == result.tld)
                .unwrap_or(usize::MAX - 1);
            aggregated_results.push(AggregatedResult {
                domain_idx,
                tld_idx,
                result,
            });
        }

        // suggestions if your domain is taken (only for single loneyly alpha wolf domain searches)
        if args.domains.len() == 1 && args.effective_suggestions() > 0 {
            if let Some(exact_tld) = specific_tld.as_deref() {
                let exact_registered = aggregated_results.iter().any(|item| {
                    item.result.name == search_name
                        && item.result.tld == exact_tld
                        && !item.result.is_available()
                });

                if exact_registered {
                    let suggestion_tlds: Vec<&'static str> = base_tlds
                        .iter()
                        .copied()
                        .filter(|tld| *tld != exact_tld)
                        .collect();

                    if !suggestion_tlds.is_empty() {
                        let suggestion_results = lookup::lookup_all(
                            &search_name,
                            &suggestion_tlds,
                            args.effective_delay(),
                            args.effective_retry(),
                            args.verbose,
                            cache_file.as_deref(),
                            false,
                            args.effective_jobs(),
                            overrides,
                            &noretry,
                            |_current, _total| {},
                        )
                        .await;

                        for result in suggestion_results
                            .into_iter()
                            .filter(|result| result.is_available())
                            .take(args.effective_suggestions())
                        {
                            let tld_idx = base_tlds
                                .iter()
                                .position(|tld| *tld == result.tld)
                                .unwrap_or(usize::MAX);
                            aggregated_results.push(AggregatedResult {
                                domain_idx,
                                tld_idx,
                                result,
                            });
                        }
                    }
                }
            }
        }
    }

    let results = sort_aggregated_results(aggregated_results);

    // save cache timestamp if we showered
    if force_refresh && paths.can_save {
        config.mark_cache_updated();
        let _ = config.save(&paths.config_file);
    }

    // print errors bruh
    output::print_errors(&results, args.verbose);

    // cuntsexv output (csv)
    if let Some(csv_opt) = &args.csv {
        match csv_opt {
            Some(path) => {
                // you wont believe this but here we are GOING to WRITE TO A FILE WITH THE CSV OUTPUT!!!! MIND BLOWN AND COCK EXPLODED
                match output::write_csv_file(&results, path) {
                    Ok(()) => eprintln!("CSV written to {}", path.display()),
                    Err(e) => eprintln!("Error writing CSV: {}", e),
                }
            }
            None => {
                // brint to terminal if user dumb and no filepath 
                output::print_csv(&results);
            }
        }
        return;
    }

    if args.show_all {
        output::print_full_table(&results, args.no_color, args.no_unicode);
    } else {
        output::print_available_table(&results, args.no_color, args.no_unicode);
    }
}

async fn run_autosearch(
    args: &Args,
    file_path: &std::path::PathBuf,
    cache_path: Option<&std::path::Path>,
    force_refresh: bool,
    overrides: &tlds::WhoisOverrides,
    noretry: &[ErrorKind],
) {
    let content = match std::fs::read_to_string(file_path) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("Could not read autosearch file: {}", e);
            return;
        }
    };

    let base_tlds = build_base_tlds(args);

    // collect all search entries and grupe them
    let mut batches: Vec<(String, Vec<String>)> = Vec::new();

    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        if let Some(first) = trimmed.chars().next() {
            if !first.is_alphanumeric() {
                continue;
            }
        }

        let (search_name, specific_tld) = if trimmed.contains('.') {
            let parts: Vec<&str> = trimmed.splitn(2, '.').collect();
            (parts[0].to_string(), Some(parts[1].to_string()))
        } else {
            (trimmed.to_string(), None)
        };

        let effective_tlds = build_effective_tlds(&base_tlds, specific_tld.as_deref());

        let entry = if let Some(pos) = batches.iter().position(|(name, _)| *name == search_name) {
            &mut batches[pos].1
        } else {
            batches.push((search_name, Vec::new()));
            &mut batches.last_mut().unwrap().1
        };
        for tld in effective_tlds {
            if !entry.contains(&tld.to_string()) {
                entry.push(tld.to_string());
            }
        }
    }

    if batches.is_empty() {
        eprintln!("No valid search terms in file");
        return;
    }

    let total_lookups: usize = batches.iter().map(|(_, tlds)| tlds.len()).sum();
    let mut completed = 0usize;
    let mut all_results: Vec<DomainResult> = Vec::new();
    let mut refresh_remaining = force_refresh;

    for (search_name, tlds) in &batches {
        let tld_refs: Vec<&str> = tlds.iter().map(|s| s.as_str()).collect();

        let results = lookup::lookup_all(
            search_name,
            &tld_refs,
            args.effective_delay(),
            args.effective_retry(),
            args.verbose,
            cache_path,
            refresh_remaining,
            args.effective_jobs(),
            overrides,
            noretry,
            |current, _total| {
                output::print_progress(completed + current, total_lookups.max(1));
            },
        )
        .await;

        refresh_remaining = false;
        completed += tlds.len();
        all_results.extend(results);
    }

    output::print_errors(&all_results, args.verbose);

    if args.show_all {
        output::print_full_table(&all_results, args.no_color, args.no_unicode);
    } else {
        output::print_available_table(&all_results, args.no_color, args.no_unicode);
    }
}

fn build_base_tlds(args: &Args) -> Vec<&'static str> {
    let tld_list = args.effective_list();
    let mut tld_vec = get_tlds_or_default(&tld_list);

    if let Some(ref only) = args.only_top {
        tld_vec = only
            .iter()
            .filter(|s| !s.is_empty())
            .map(|s| -> &'static str { Box::leak(s.clone().into_boxed_str()) })
            .collect();
    }

    if let Some(ref top) = args.top_tlds {
        tld_vec = apply_top_tlds(tld_vec, top);
    }

    tld_vec
}

fn parse_domain_input(raw_domain: &str) -> (String, Option<String>) {
    if raw_domain.contains('.') {
        let parts: Vec<&str> = raw_domain.splitn(2, '.').collect();
        (parts[0].to_string(), Some(parts[1].to_string()))
    } else {
        (raw_domain.to_string(), None)
    }
}

fn build_effective_tlds(
    base_tlds: &[&'static str],
    specific_tld: Option<&str>,
) -> Vec<&'static str> {
    if let Some(tld) = specific_tld {
        vec![Box::leak(tld.to_string().into_boxed_str()) as &'static str]
    } else {
        base_tlds.to_vec()
    }
}

fn estimate_total_lookups(domains: &[String], base_tlds: &[&'static str]) -> usize {
    domains
        .iter()
        .map(|domain| {
            if domain.contains('.') {
                1
            } else {
                base_tlds.len()
            }
        })
        .sum()
}

fn sort_aggregated_results(mut aggregated: Vec<AggregatedResult>) -> Vec<DomainResult> {
    aggregated.sort_by(|a, b| {
        a.tld_idx
            .cmp(&b.tld_idx)
            .then(a.domain_idx.cmp(&b.domain_idx))
    });
    aggregated.into_iter().map(|item| item.result).collect()
}
