Fix the UI code not displaying any channel names
use crate::models::ChannelInfo;
use crate::models::PatternGroup;
use colored::ColoredString;
use colored::Colorize;
use crossterm::style::Stylize;
use prettytable::{row, Table};
use tui::text::Span;
use tui::text::Spans;
use std::io;
use tui::{
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    widgets::{Block, Borders, Cell, Paragraph, Row as TuiRow, Table as TuiTable, Tabs},
    Frame, Terminal,
};

pub enum Page {
    Overview,
    Channels,
    Patterns,
}

pub struct App<'a> {
    pub page: Page,
    pub patterns: Vec<PatternGroup>,
    pub channels: Vec<ChannelInfo>,
    pub selected: usize,
    pub input: &'a str,
}

impl<'a> App<'a> {
    pub fn new(patterns: Vec<PatternGroup>, channels: Vec<ChannelInfo>) -> App<'a> {
        App {
            page: Page::Overview,
            patterns,
            channels,
            selected: 0,
            input: "",
        }
    }

    pub fn next(&mut self) {
        self.selected = (self.selected + 1) % 3;
    }

    pub fn previous(&mut self) {
        if self.selected > 0 {
            self.selected -= 1;
        }
    }
}

pub fn run_ui(patterns: Vec<PatternGroup>, channels: Vec<ChannelInfo>) -> Result<(), io::Error> {
    // Set up terminal
    let stdout = io::stdout();
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    // Create app and run it
    let app = App::new(patterns, channels);
    let res = tui_run(&mut terminal, app);

    // Restore terminal
    terminal.show_cursor()?;

    res
}

fn tui_run<B: tui::backend::Backend + std::io::Write>(
    terminal: &mut Terminal<B>,
    mut app: App,
) -> Result<(), io::Error> {
    use crossterm::{
        event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
        execute,
        terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    };
    use std::time::{Duration, Instant};

    enable_raw_mode()?;
    execute!(terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture)?;

    let tick_rate = Duration::from_millis(250);
    let mut last_tick = Instant::now();

    loop {
        terminal.draw(|f| ui(f, &app))?;

        let timeout = tick_rate
            .checked_sub(last_tick.elapsed())
            .unwrap_or_else(|| Duration::from_secs(0));

        if event::poll(timeout)? {
            if let Event::Key(key) = event::read()? {
                match key.code {
                    KeyCode::Char('q') => {
                        disable_raw_mode()?;
                        execute!(
                            terminal.backend_mut(),
                            LeaveAlternateScreen,
                            DisableMouseCapture
                        )?;
                        break;
                    }
                    KeyCode::Tab => {
                        app.next();
                    }
                    KeyCode::BackTab => {
                        app.previous();
                    }
                    _ => {}
                }
            }
        }

        if last_tick.elapsed() >= tick_rate {
            last_tick = Instant::now();
        }
    }

    Ok(())
}

fn ui<B: tui::backend::Backend>(f: &mut Frame<B>, app: &App) {
    // Define layout
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(1)
        .constraints(
            [
                Constraint::Length(3),
                Constraint::Min(2),
            ]
            .as_ref(),
        )
        .split(f.size());

    // Render tabs
    let titles = vec!["Overview", "Channels", "Patterns"]
        .iter()
        .map(|t| (*t).to_string())
        .collect::<Vec<_>>();
    let tabs = Tabs::new(titles.iter().map(|t| Spans::from(t.clone())).collect())
        .select(match app.page {
            Page::Overview => 0,
            Page::Channels => 1,
            Page::Patterns => 2,
        })
        .block(Block::default().borders(Borders::ALL).title("Pages"))
        .highlight_style(Style::default().fg(Color::Yellow))
        .style(Style::default().fg(Color::White));

    f.render_widget(tabs, chunks[0]);

    // Render content based on selected page
    match app.page {
        Page::Overview => render_overview(f, app, chunks[1]),
        Page::Channels => render_channels(f, app, chunks[1]),
        Page::Patterns => render_patterns(f, app, chunks[1]),
    }
}

fn render_overview<B: tui::backend::Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
    let total_channels = app.channels.len();
    let feature_channels = app.patterns
        .iter()
        .find(|p| p.pattern.to_lowercase().contains("feature"))
        .map(|p| p.channels.len())
        .unwrap_or(0);

    let paragraph = Paragraph::new(format!(
        "Total Valid Channels: {}\nChannels with 'feature' in their name: {}",
        total_channels, feature_channels
    ))
    .block(Block::default().borders(Borders::ALL).title("Overview"));

    f.render_widget(paragraph, area);
}

fn render_channels<B: tui::backend::Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
    // Create a table
    let header = ["Channel", "Roblox.exe", "DeployHistory.txt", "Mac DeployHistory.txt", "ClientSettings"]
        .iter()
        .cloned()
        .map(Cell::from)
        .collect::<Vec<_>>();
    
    let rows = app.channels.iter().map(|c| {
        TuiRow::new(vec![
            c.colored_channel.clone().to_string().into(),
            bool_to_cell(c.roblox_exe),
            bool_to_cell(c.deploy_history),
            bool_to_cell(c.mac_deploy_history),
            bool_to_cell(c.client_settings),
        ])
    });

    let table = TuiTable::new(rows)
        .header(
            TuiRow::new(header)
                .style(Style::default().fg(Color::Yellow))
                .bottom_margin(1),
        )
        .block(Block::default().borders(Borders::ALL).title("Channels"))
        .widths(&[
            Constraint::Percentage(25),
            Constraint::Percentage(15),
            Constraint::Percentage(15),
            Constraint::Percentage(20),
            Constraint::Percentage(25),
        ]);
    
    f.render_widget(table, area);
}

fn bool_to_cell(value: bool) -> Cell<'static> {
    if value {
        Cell::from("✅")
    } else {
        Cell::from("❌")
    }
}

fn render_patterns<B: tui::backend::Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
    let block = Block::default().borders(Borders::ALL).title("Pattern Groups");
    let inner = block.inner(area);
    f.render_widget(block, area);

    let text = app.patterns.iter().fold(String::new(), |acc, pattern| {
        format!(
            "{}Pattern: {}\nChannels: {}\n\n",
            acc,
            pattern.pattern.clone().bold().underlined(),
            pattern.channels.join(", ")
        )
    });

    let paragraph = Paragraph::new(text)
        .block(Block::default())
        .wrap(tui::widgets::Wrap { trim: true });

    f.render_widget(paragraph, inner);
}

mod checker;
mod cli;
mod errors;
mod models;
mod pattern;
mod ui;

use checker::check_channel;
use cli::parse_args;
use errors::AppError;
use indicatif::{ProgressBar, ProgressStyle};
use itertools::Itertools;
use models::ChannelInfo;
use rayon::prelude::*;
use reqwest::blocking::Client;
use std::collections::HashSet;
use std::fs;
use std::io::{self, BufRead};
use std::path::Path;
use std::time::Duration;
use tracing::{error, info};
use tracing_subscriber::fmt::format::FmtSpan;
use ui::run_ui;
use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;

fn main() -> Result<(), AppError> {
    // Initialize logging
    tracing_subscriber::fmt()
        .with_span_events(FmtSpan::CLOSE) // Log span end events
        .with_max_level(tracing::Level::INFO)
        .init();

    // Parse command-line arguments
    let matches = parse_args();

    // Set verbosity
    if matches.get_one::<String>("verbose").is_some() {
        tracing::subscriber::with_default(tracing_subscriber::fmt::Subscriber::new(), || {
            info!("Verbose mode enabled");
        });
    }

    // 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);
        info!("Augmented wordlist with 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);
        info!("Augmented wordlist with 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);
        info!("Augmented wordlist with {} random strings.", 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.");

    info!("Analyzing channel patterns...");
    let patterns = pattern::analyze_patterns(&results.iter().map(|c| c.original_channel.clone()).collect::<Vec<_>>());

    // Display results in advanced UI
    run_ui(patterns.clone(), results.clone())?;

    // 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> {
    info!("Reading wordlist from file: {:?}", path.as_ref());
    let file = fs::File::open(path)?;
    let reader = io::BufReader::new(file);
    let wordlist = reader
        .lines()
        .filter_map(Result::ok)
        .filter(|line| !line.trim().is_empty())
        .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);
    }
}

// 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(())
}

use crate::{
    errors::AppError,
    models::ChannelInfo,
};
use colored::ColoredString;
use colored::Colorize;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use retry::{delay::Exponential, retry};
use serde_json::Value;
use tracing::{debug, error, info};
use regex::Regex;

pub 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() {
                Ok(response) => {
                    if response.status() == StatusCode::OK {
                        if *name == "ClientSettings" {
                            // Check ClientSettings response body
                            match response.json::<Value>() {
                                Ok(json) => {
                                    // Assuming valid if "applicationSettings" is not null
                                    let is_valid = !json
                                        .get("applicationSettings")
                                        .map(|v| v.is_null())
                                        .unwrap_or(true);
                                    Ok(is_valid)
                                }
                                Err(e) => {
                                    error!("Failed to parse JSON from {}: {}", url, e);
                                    Ok(false)
                                }
                            }
                        } else {
                            Ok(true)
                        }
                    } 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.to_lowercase().contains("feature")
        || channel.to_lowercase().contains("project")
        || channel.to_lowercase().contains("bugfix")
    {
        channel.bold().blue()
    } else if channel.to_lowercase().contains("canary") {
        channel.bold().yellow()
    } else {
        channel.red()
    }
}