whoxydse 0.1.1

Discover related top-level domains using Whoxy API: historical WHOIS, reverse WHOIS, and DNS verification
mod api;
mod config;
mod dns;
mod models;
mod output;
mod pivot;

use anyhow::{Context, Result};
use clap::Parser;
use models::DiscoveryStats;
use output::*;

#[tokio::main]
async fn main() -> Result<()> {
    // Parse CLI arguments
    let config = config::Config::try_parse().context("Failed to parse CLI arguments")?;
    let pivot_config = config.pivot_config();

    // Display welcome message
    welcome(&config.domain);

    // Initialize API client
    let client = api::WhoxyClient::new(config.api_key(), config.verbose);

    // Preflight check: Test API availability and fetch WHOIS data
    progress("Performing preflight check (API + WHOIS lookup)...");
    let api_availability = client.preflight_check(&config.domain).await;
    
    // Display API availability status
    if api_availability.history_api {
        success("✓ History API is available");
    } else {
        if config.continue_on_fail {
            warning("✗ History API unavailable - will skip");
        } else {
            error("✗ History API unavailable - API key may be invalid");
            eprintln!("\n💡 Tip: Use --continue-on-fail to proceed with available APIs");
            return Ok(());
        }
    }

    if api_availability.reverse_whois_api {
        success("✓ Reverse WHOIS API is available");
    } else {
        if config.continue_on_fail {
            warning("✗ Reverse WHOIS API unavailable - will skip");
        } else {
            error("✗ Reverse WHOIS API unavailable - API key may be invalid");
            eprintln!("\n💡 Tip: Use --continue-on-fail to proceed with available APIs");
            return Ok(());
        }
    }

    // Check if we have WHOIS attributes to work with
    let has_whois_attrs = api_availability.whois_attributes.as_ref()
        .map(|attrs| !attrs.names.is_empty() || !attrs.emails.is_empty() || !attrs.companies.is_empty())
        .unwrap_or(false);

    // If neither API is available, check if we can still proceed with attributes
    if !api_availability.history_api && !api_availability.reverse_whois_api {
        if has_whois_attrs && config.continue_on_fail {
            // We have attributes from WHOIS scraping, allow reverse WHOIS to proceed
            // (it might work even if preflight said it wouldn't)
            warning("Preflight indicated APIs unavailable, but proceeding with extracted identifiers...");
        } else {
            error("No APIs are available. Cannot proceed.");
            if has_whois_attrs {
                eprintln!("\n💡 Tip: Use --continue-on-fail to attempt reverse WHOIS with extracted identifiers");
            }
            return Ok(());
        }
    }

    // Display WHOIS attributes found during preflight
    if let Some(ref whois_attrs) = api_availability.whois_attributes {
        if !whois_attrs.names.is_empty() || !whois_attrs.emails.is_empty() || !whois_attrs.companies.is_empty() {
            progress("WHOIS identifiers extracted during preflight:");
            let mut display_attrs = models::PivotAttributes::default();
            display_attrs.merge(whois_attrs.clone());
            display_pivot_attributes(&display_attrs, &pivot_config);
        }
    }

    // Step 1: Fetch WHOIS history (only if available)
    let history_records = if api_availability.history_api {
        progress(&format!("Fetching WHOIS history for {}...", config.domain));
        let history_response = client
            .get_whois_history(&config.domain)
            .await
            .context("Failed to fetch WHOIS history")?;

        if history_response.status != 1 {
            // Show detailed error message if available
            if let Some(ref status_reason) = history_response.status_reason {
                if status_reason.to_lowercase().contains("zero account balance") {
                    error(&format!("API account has zero balance: {}", status_reason));
                } else if let Some(ref error_msg) = history_response.error {
                    error(&format!("Failed to fetch WHOIS history: {}", error_msg));
                } else {
                    error(&format!("Failed to fetch WHOIS history: {}", status_reason));
                }
            } else if let Some(ref error_msg) = history_response.error {
                error(&format!("Failed to fetch WHOIS history: {}", error_msg));
            } else {
                // Check if it might be "no history found" vs actual error
                if history_response.history.is_empty() && history_response.status == 0 {
                    warning("No historical WHOIS records found for this domain in Whoxy's database.");
                    warning("This could mean:");
                    warning("  - The domain has no historical records");
                    warning("  - The API key is invalid (check your key)");
                    warning("  - The domain is not in Whoxy's database");
                } else {
                    error(&format!("Failed to fetch WHOIS history - API returned status {}", history_response.status));
                }
            }
            
            Vec::new()
        } else {
            let records = history_response.history;
            success(&format!(
                "Found {} historical WHOIS record(s)",
                records.len()
            ));
            records
        }
    } else {
        warning("Skipping WHOIS history fetch (API unavailable)");
        Vec::new()
    };

    // Step 2: Collect pivot attributes from multiple sources
    let mut attributes = models::PivotAttributes::default();

    // First, add manual identifiers if provided
    if !config.search_names.is_empty() || !config.search_emails.is_empty() || !config.search_companies.is_empty() {
        progress("Using manual identifiers for reverse WHOIS...");
        for name in &config.search_names {
            attributes.add_name(name.trim().to_string());
        }
        for email in &config.search_emails {
            attributes.add_email(email.trim().to_lowercase());
        }
        for company in &config.search_companies {
            attributes.add_company(company.trim().to_string());
        }
    }

    // Second, use WHOIS attributes from preflight (already fetched) - THIS IS THE PRIMARY SOURCE
    if let Some(ref whois_attrs) = api_availability.whois_attributes {
        if !whois_attrs.names.is_empty() || !whois_attrs.emails.is_empty() || !whois_attrs.companies.is_empty() {
            progress("Using WHOIS identifiers from preflight check...");
            attributes.merge(whois_attrs.clone());
        }
    }

    // Third, extract from historical records if available (supplemental)
    if !history_records.is_empty() {
        progress("Extracting pivotable attributes from historical records...");
        let history_attrs = pivot::extract_pivot_attributes(&history_records);
        attributes.merge(history_attrs);
    }

    // Display discovered attributes
    if !attributes.names.is_empty() || !attributes.emails.is_empty() || !attributes.companies.is_empty() {
        display_pivot_attributes(&attributes, &pivot_config);
    }

    // Check if we have any attributes to pivot on
    let has_attributes = (pivot_config.use_name && !attributes.names.is_empty())
        || (pivot_config.use_email && !attributes.emails.is_empty())
        || (pivot_config.use_company && !attributes.companies.is_empty());

    if !has_attributes {
        error("No pivotable attributes found. Cannot perform reverse WHOIS lookups.");
        eprintln!("\n💡 Options:");
        eprintln!("   1. Use --search-name, --search-email, or --search-company to manually specify identifiers");
        eprintln!("   2. Ensure the domain has WHOIS history or current WHOIS data available");
        eprintln!("   3. Ensure the system 'whois' command is available for current WHOIS lookup");
        return Ok(());
    }

    // Display what we'll search for
    if config.verbose {
        progress("Attributes to use for reverse WHOIS:");
        if pivot_config.use_name && !attributes.names.is_empty() {
            eprintln!("  Names: {:?}", attributes.names);
        }
        if pivot_config.use_email && !attributes.emails.is_empty() {
            eprintln!("  Emails: {:?}", attributes.emails);
        }
        if pivot_config.use_company && !attributes.companies.is_empty() {
            eprintln!("  Companies: {:?}", attributes.companies);
        }
    }

    // Warn when pivot attributes don't match the target domain (lower-fidelity results)
    if pivot::has_low_fidelity_pivots(&config.domain, &attributes, &pivot_config) {
        output::display_fidelity_warning();
    }

    // Step 3: Perform reverse WHOIS lookups
    // Try reverse WHOIS if we have attributes, even if preflight said API unavailable
    // (preflight might have been wrong, or API might work for reverse WHOIS even if history failed)
    let discovered_domains = if has_attributes {
        progress("Performing reverse WHOIS lookups...");
        if !api_availability.reverse_whois_api {
            warning("Preflight indicated reverse WHOIS API unavailable, but attempting anyway with extracted identifiers...");
        }
        match pivot::discover_domains(&client, &attributes, &pivot_config).await {
            Ok(domains) => domains,
            Err(e) => {
                error(&format!("Reverse WHOIS lookup failed: {}", e));
                if !api_availability.reverse_whois_api {
                    eprintln!("\n💡 The API may require account balance or valid API key.");
                    eprintln!("   Check your Whoxy account balance and API key.");
                }
                Vec::new()
            }
        }
    } else {
        warning("Skipping reverse WHOIS lookups (no attributes available)");
        Vec::new()
    };

    let total_found = discovered_domains.len();
    success(&format!("Found {} domain(s) from reverse WHOIS", total_found));

    if discovered_domains.is_empty() {
        warning("No domains discovered via reverse WHOIS.");
        return Ok(());
    }

    // Step 4: De-duplicate domains
    progress("De-duplicating discovered domains...");
    let deduplicated = pivot::deduplicate_domains(discovered_domains);
    let after_dedup = deduplicated.len();
    success(&format!(
        "After de-duplication: {} domain(s)",
        after_dedup
    ));

    // Step 5: DNS verification
    progress("Verifying domains via DNS lookup...");
    let verified = dns::verify_domains_concurrent(deduplicated).await;
    let after_dns = verified.len();
    success(&format!(
        "After DNS verification: {} domain(s)",
        after_dns
    ));

    // Step 6: Display results
    display_domains(&verified);

    // Display statistics
    let stats = DiscoveryStats {
        total_domains_found: total_found,
        after_deduplication: after_dedup,
        after_dns_verification: after_dns,
        domains_by_attribute: std::collections::HashMap::new(), // Could be enhanced to track this
    };
    display_stats(&stats);

    // Final summary
    summary(after_dns);

    Ok(())
}