use clap::{Arg, Command};
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use itertools::Itertools;
use prettytable::{row, Table};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rayon::prelude::*;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use retry::{delay::Exponential, retry};
use serde::Serialize;
use std::collections::HashSet;
use std::error::Error;
use std::fs;
use std::io::{self, BufRead};
use std::path::Path;
use std::time::Duration;
use thiserror::Error;
use tracing::{debug, error, info};
use tracing_subscriber::fmt::format::FmtSpan;

fn main() -> Result<(), Box<dyn Error>> {
    // Initialize logging
    tracing_subscriber::fmt()
        .with_span_events(FmtSpan::CLOSE) // Log span end events
        .init();

    // Parse command-line arguments
    let matches = Command::new("Roblox Channel Checker")
        .version("1.0")
        .author("Your Name <youremail@example.com>")
        .about("Checks for valid Roblox channels based on wordlist combinations")
        .arg(
            Arg::new("input")
                .short('i')
                .long("input")
                .value_name("FILE")
                .help("Sets a custom input wordlist file"),
        )
        .arg(
            Arg::new("output")
                .short('o')
                .long("output")
                .value_name("FILE")
                .help("Sets an output file to save results in JSON format"),
        )
        .arg(
            Arg::new("concurrency")
                .short('c')
                .long("concurrency")
                .value_name("NUMBER")
                .help("Sets the level of concurrency for HTTP requests")
                .default_value("8"),
        )
        .arg(
            Arg::new("timeout")
                .short('t')
                .long("timeout")
                .value_name("SECONDS")
                .help("Sets the timeout for HTTP requests in seconds")
                .default_value("10"),
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .help("Increases verbosity level"),
        )
        .arg(
            Arg::new("prefixes")
                .short('p')
                .long("prefixes")
                .value_name("PREFIXES")
                .help("Comma-separated list of prefixes to add to words"),
        )
        .arg(
            Arg::new("suffixes")
                .short('s')
                .long("suffixes")
                .value_name("SUFFIXES")
                .help("Comma-separated list of suffixes to add to words"),
        )
        .arg(
            Arg::new("random-strings")
                .short('r')
                .long("random-strings")
                .value_name("NUMBER")
                .help("Number of random alphanumeric strings to generate and add to the wordlist"),
        )
        .get_matches();

    // Read wordlist from file or stdin
    let wordlist = if let Some(input_path) = matches.get_one::<String>("input") {
        read_wordlist_from_file(input_path)?
    } else {
        info!("Reading wordlist from stdin. Enter words followed by Enter. Press Ctrl+D (Unix) or Ctrl+Z (Windows) to finish:");
        read_wordlist_from_stdin()?
    };

    // Deduplicate wordlist
    let mut unique_wordlist: Vec<String> = wordlist.into_iter().unique().collect();
    unique_wordlist.sort();

    // Wordlist Augmentation
    if let Some(prefixes_str) = matches.get_one::<String>("prefixes") {
        let prefixes: Vec<&str> = prefixes_str.split(',').collect();
        augment_wordlist_with_prefixes(&mut unique_wordlist, &prefixes);
    }

    if let Some(suffixes_str) = matches.get_one::<String>("suffixes") {
        let suffixes: Vec<&str> = suffixes_str.split(',').collect();
        augment_wordlist_with_suffixes(&mut unique_wordlist, &suffixes);
    }

    if let Some(num_random_str) = matches.get_one::<String>("random-strings") {
        let num_random: usize = num_random_str.parse().unwrap_or(0);
        augment_wordlist_with_random_strings(&mut unique_wordlist, num_random);
    }

    if unique_wordlist.is_empty() {
        error!("Wordlist is empty. Please provide at least one word.");
        return Ok(());
    }

    info!("Generating combinations...");
    let combinations = generate_combinations(&unique_wordlist);
    info!("Total combinations generated: {}", combinations.len());

    // Configure HTTP client
    let timeout = matches
        .get_one::<String>("timeout")
        .unwrap()
        .parse::<u64>()
        .unwrap_or(10);
    let client = Client::builder()
        .timeout(Duration::from_secs(timeout))
        .build()?;

    // Setup progress bar
    let pb = ProgressBar::new(combinations.len() as u64);
    pb.set_style(
        ProgressStyle::default_bar()
            .template(
                "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
            )
            .unwrap()
            .progress_chars("#>-"),
    );

    let concurrency = matches
        .get_one::<String>("concurrency")
        .unwrap()
        .parse::<usize>()
        .unwrap_or(8);
    rayon::ThreadPoolBuilder::new()
        .num_threads(concurrency)
        .build_global()
        .unwrap();

    info!(
        "Starting channel checks with concurrency level: {}",
        concurrency
    );

    // Process combinations in parallel
    let results: Vec<ChannelInfo> = combinations
        .par_iter()
        .map(|combination| {
            let result = check_channel(&client, combination);
            pb.inc(1);
            result
        })
        .filter_map(|res| res)
        .collect();

    pb.finish_with_message("Channel checks completed.");

    // Display results in table format
    if results.is_empty() {
        info!("No valid channels found.");
    } else {
        let mut table = Table::new();
        table.add_row(row![
            "Channel",
            "Roblox.exe",
            "DeployHistory.txt",
            "Mac DeployHistory.txt",
            "ClientSettings",
        ]);
        for info in &results {
            table.add_row(row![
                info.colored_channel,
                info.roblox_exe,
                info.deploy_history,
                info.mac_deploy_history,
                info.client_settings,
            ]);
        }
        table.printstd();
    }

    // Save results to file if output path is provided
    if let Some(output_path) = matches.get_one::<String>("output") {
        info!("Saving results to {}", output_path);
        save_results_to_file(&results, output_path)?;
    }

    Ok(())
}

// Reads wordlist from a file
fn read_wordlist_from_file<P: AsRef<Path>>(path: P) -> Result<Vec<String>, AppError> {
    let wordlist = fs::read_to_string(path)?
        .lines()
        .filter_map(|line| {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                None
            } else {
                Some(trimmed.to_string())
            }
        })
        .collect();
    Ok(wordlist)
}

// Reads wordlist from standard input
fn read_wordlist_from_stdin() -> Result<Vec<String>, AppError> {
    let stdin = io::stdin();
    let wordlist = stdin
        .lock()
        .lines()
        .filter_map(Result::ok)
        .filter(|line| !line.trim().is_empty())
        .collect();
    Ok(wordlist)
}

// Generates combinations of words
fn generate_combinations(wordlist: &[String]) -> HashSet<String> {
    let mut combinations = HashSet::new();
    let max_combination_length = 3; // Adjust as needed

    for length in 1..=max_combination_length {
        for combo in wordlist.iter().permutations(length) {
            let concatenated = combo.into_iter().join("");
            combinations.insert(concatenated);
        }
    }

    combinations
}

// Augments the wordlist with prefixes
fn augment_wordlist_with_prefixes(wordlist: &mut Vec<String>, prefixes: &[&str]) {
    let mut new_words = Vec::new();
    for prefix in prefixes {
        for word in wordlist.iter() {
            new_words.push(format!("{}{}", prefix, word));
        }
    }
    wordlist.append(&mut new_words);
}

// Augments the wordlist with suffixes
fn augment_wordlist_with_suffixes(wordlist: &mut Vec<String>, suffixes: &[&str]) {
    let mut new_words = Vec::new();
    for suffix in suffixes {
        for word in wordlist.iter() {
            new_words.push(format!("{}{}", word, suffix));
        }
    }
    wordlist.append(&mut new_words);
}

// Augments the wordlist with random strings
fn augment_wordlist_with_random_strings(wordlist: &mut Vec<String>, num_strings: usize) {
    let rng = thread_rng();
    for _ in 0..num_strings {
        let random_string: String = rng
            .clone()
            .sample_iter(&Alphanumeric)
            .take(8) // Adjust the length of random strings as needed
            .map(char::from)
            .collect();
        wordlist.push(random_string);
    }
}

// Checks a single channel and returns ChannelInfo if valid
fn check_channel(client: &Client, channel_name: &str) -> Option<ChannelInfo> {
    let base_url = format!("http://setup.rbxcdn.com/channel/z{}", channel_name);
    let endpoints = [
        ("Roblox.exe", format!("{}/Roblox.exe", base_url)),
        (
            "DeployHistory.txt",
            format!("{}/DeployHistory.txt", base_url),
        ),
        (
            "Mac DeployHistory.txt",
            format!("{}/mac/DeployHistory.txt", base_url),
        ),
        (
            "ClientSettings",
            format!(
"http://clientsettings.roblox.com/v2/settings/application/RCCBootstrapper/bucket/z{}",
channel_name
),
        ),
    ];

    let mut results = vec![];

    for (name, url) in &endpoints {
        let result = retry(Exponential::from_millis(500).take(3), || {
            debug!("Checking {} at {}", name, url);
            match client.get(url).send() {
                // Use GET instead of HEAD to get the response body
                Ok(response) => {
                    if response.status() == StatusCode::OK {
                        if *name == "ClientSettings" {
                            // Check ClientSettings response body
                            let body = response.text().unwrap_or_default();
                            Ok(!body.contains(r#"{"applicationSettings":null}"#))
                        // Valid if not null
                        } else {
                            Ok(true) // Other endpoints are valid if status is OK
                        }
                    } else {
                        Ok(false)
                    }
                }
                Err(e) => {
                    error!("Error checking {}: {}", url, e);
                    Err(e)
                }
            }
        })
        .unwrap_or(false);
        results.push(result);
    }

    if results[0] {
        Some(ChannelInfo {
            colored_channel: color_code_channel(channel_name),
            original_channel: channel_name.to_string(),
            roblox_exe: results[0],
            deploy_history: results[1],
            mac_deploy_history: results[2],
            client_settings: results[3],
        })
    } else {
        None
    }
}

fn color_code_channel(channel: &str) -> ColoredString {
    if channel.contains("feature") || channel.contains("project") || channel.contains("bugfix") {
        channel.bold().blue()
    } else {
        channel.red()
    }
}
// Saves results to a JSON file
fn save_results_to_file(results: &[ChannelInfo], path: &str) -> Result<(), AppError> {
    let json = serde_json::to_string_pretty(results)?;
    fs::write(path, json)?;
    Ok(())
}

#[derive(Debug, Serialize)]
struct ChannelInfo {
    #[serde(skip_serializing)]
    colored_channel: ColoredString,
    #[serde(rename = "channel")]
    original_channel: String,
    roblox_exe: bool,
    deploy_history: bool,
    mac_deploy_history: bool,
    client_settings: bool,
}

#[derive(Debug, Error)]
enum AppError {
    #[error("IO error occurred: {0}")]
    Io(#[from] io::Error),
    #[error("HTTP request error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("Serialization error: {0}")]
    Serde(#[from] serde_json::Error),
}


Make this program way more advanced, no config file.
Remove things such as `--` `__` in channel names like z__banana or z--banana <- Both - and _ aren't in the first word persay.
remove duplicate words like zbananabanana from happening in the same thing