Support more than just zfeature{}
Make channel generation and verification even more advanced.
Provide an example config file aswell
Use this list of known channels to help you with formulating the algorithm.
[
    "Common",
    "Ganesh",
    "LIVE",
    "ZActivityHistory",
    "ZActivityHistory2",
    "ZActivityHistory3",
    "ZAddCloseReasonAPIPrivate",
    "ZAFlag",
    "ZAIDiff",
    "ZAIDiffMessage",
    "ZAllocateCacheContiguousWithConstraint",
    "ZAllocCallbacks",
    "ZAllocTrackWinQuickFix",
    "ZAnrReporterOnWindowsTesting",
    "ZAssetAccessErrorMessageImprovements",
    "ZAssetCacheInitProd",
    "ZAssetDeliveryV2",
    "ZAssetPreloading",
    "ZAsyncApolloCow",
    "ZAsyncApolloCowExperiment",
    "ZAudioTestsAndExperiments",
    "ZAudioQualityTesting",
    "ZAutoSetup",
    "ZAutoSetup-Alpha",
    "ZAvatarEmotion",
    "ZAvatarPathFinding",
    "ZAvatarRelease",
    "ZAvatarTeam",
    "ZAvatarTeam2",
    "ZBaseRCU",
    "ZBeta",
    "ZBillTest",
    "ZBisect1",
    "ZBisect2",
    "ZBisect3",
    "ZBisect4",
    "ZBisect5",
    "ZBisect6",
    "ZBugfixAccessory-Welds",
    "ZBugfixAssorted-Channel-Test-Changes",
    "ZBugfixBoost-Mutex-Revert",
    "ZBugfixCLI-54676-Test",
    "ZBugfixCLI-55214-Master",
    "ZBugfixQuaternionChannels",
    "ZBugfixSanity-Check",
    "ZBugfixTruss-Crash",
    "ZBuilderFont",
    "ZCanary",
    "ZCanary1",
    "ZCanary2",
    "ZCanary3",
    "ZCanary4",
    "ZCanary5",
    "ZCanary6",
    "ZCanary7",
    "ZCanary8",
    "ZCanary9",
    "ZCanaryApps",
    "ZCanaryTest1",
    "ZCanaryTest2",
    "ZCanaryTest3",
    "ZCanaryTest4",
    "ZCanaryTest5",
    "ZCanaryTest6",
    "ZCandidateFixIC",
    "ZCandidateFixIC2",
    "ZCandidateFixIC3",
    "ZCanidateFixIC3",
    "ZCaptures",
    "ZCheckSendTypeBeforeSetupTimer",
    "ZCheckSendTypeBeforeSetUptimerInHttpPointsReporter",
    "ZChunkExperimentBinary",
    "ZChunkExperimentBinaryControl",
    "ZCLI-92373",
    "ZCLI-92373-2",
    "ZCLI-97809",
    "ZCLI109812",
    "ZCLI111800",
    "ZClientIntegration",
    "ZClientIntegration1",
    "ZClientPresence2",
    "ZClientTokenFromHeader3",
    "ZClientWatcher",
    "ZClockSkew",
    "ZCommonCLFB",
    "ZCompatibilityTelemetryChannelFlagOnly",
    "ZControl",
    "ZConvAICodeRunner",
    "ZCopy_Both",
    "ZCrashHandlerShutdownTesting",
    "ZCrashOnShutdown",
    "ZCreateMRDAllocsReduc",
    "ZCSCTesting",
    "ZCSG4",
    "ZCSPUHotfixTest",
    "ZDeeplinkPrototype",
    "ZDisableMemoryTrackingTest",
    "ZDisableMemoryTrackingTestComparison",
    "ZDMAccessorMigrations",
    "ZDontRerenderForBadTexture",
    "ZDraggerImprovements",
    "ZDummy",
    "ZDummy3",
    "ZDXT",
    "ZEnableZstdForClientSettings",
    "ZExpControl",
    "ZExpControl1",
    "ZExpControl2",
    "ZExpControl3",
    "ZExpControlStandby",
    "ZExpControlTest",
    "ZExpD1",
    "ZExpD256",
    "ZExperienceCoordinator",
    "ZExpR256",
    "ZExpVariantPrivate",
    "ZExpVariantPrivate613",
    "ZExpVariantPublic",
    "ZExpVariantPublic3",
    "ZExpVariantStandby",
    "ZFallThroughCleanup",
    "ZFCCache",
    "ZFCThreadSafe",
    "ZFeature600-Public-URE-Testing",
    "ZFeature610Curl680",
    "ZFeature613Curl860",
    "ZFeature613Curl860-2",
    "ZFeature618Cares",
    "ZFeatureAbstractMetricStorage2",
    "ZFeatureAbstractSessionMetricStorage",
    "ZFeatureAeroChannel",
    "ZFeatureAeroChannel2",
    "ZFeatureAerodynamics",
    "ZFeatureAlwaysStrafing",
    "ZFeatureAlwaysStrafing2",
    "ZFeatureAndroidDeeplink",
    "ZFeatureAndroid-Crash-Fix",
    "ZFeatureAsset-Access-Error-Message-Improvements",
    "ZFeatureAudioDiscovery",
    "ZFeatureAudioMetadata",
    "ZFeatureAutoReconnect",
    "ZFeatureAvatarCreationService",
    "ZFeatureBaseline",
    "ZFeatureBoost_Removal_Test_In_Prod",
    "ZFeatureBootstrapper_Fix",
    "ZFeatureChannel_Test",
    "ZFeatureCharacter-Controller",
    "ZFeatureCLI-88773-Curl-Update",
    "ZFeatureCLI-88773-Curl-Update-2",
    "ZFeatureCLI-88773-Curl-Update-3",
    "ZFeatureCrash-On-Shutdown",
    "ZFeatureDeeplink",
    "ZFeatureDynamicHeadExportTool",
    "ZFeatureEnablingNewCSGApis",
    "ZFeatureFF1",
    "ZFeatureFiber",
    "ZFeatureFMOD-20109",
    "ZFeatureFMOD-20115",
    "ZFeatureFMOD-Recording-Test",
    "ZFeatureGame-Engine-ML",
    "ZFeatureGrowth",
    "ZFeatureHackWeek-2022Group-Teleport-Improvements",
    "ZFeatureHarmony",
    "ZFeatureHon",
    "ZFeatureHSR2CDNPlayTest",
    "ZFeatureHSR2CDNPlayTest2",
    "ZFeatureHumanoidDescriptionInstanceSupport",
    "ZFeatureHyperion-Test",
    "ZFeatureHypTelem616",
    "ZFeatureIdle-Mode",
    "ZFeatureInExperienceCreation",
    "ZFeatureInstance-Parent-Weak-Ptr",
    "ZFeatureInstance-Parent-Weak-Ptr-2",
    "ZFeatureKeyboard-Navigation",
    "ZFeatureLTCG1",
    "ZFeatureLatency_Log",
    "ZFeatureLiveParts",
    "ZFeatureLuaCharacterController",
    "ZFeatureLuaIInline1",
    "ZFeatureLuaObjectInstanceBridgeMergeFix",
    "ZFeatureLuaSM",
    "ZFeatureLUATOOLS-121-rbxpMoonbeamReleaseTest",
    "ZFeatureLua-File-Sync",
    "ZFeatureMarkVisibleBitForUILayout-618",
    "ZFeatureMesh-Compression",
    "ZFeatureModel-Scale",
    "ZFeatureOnboarding",
    "ZFeatureOpenXR",
    "ZFeaturePackagePublishTest",
    "ZFeaturePathEdit",
    "ZFeaturePC-VR",
    "ZFeaturePlane-Handles",
    "ZFeaturePrepare-Shorelines",
    "ZFeaturePrepare-Shorelines2",
    "ZFeaturePrepare-Shorelines3",
    "ZFeaturePrepare-Shorelines4",
    "ZFeaturePresence-Manager",
    "ZFeaturePrototypeOnboarding",
    "ZFeatureQt5.15",
    "ZFeatureQuest_VR_Demo",
    "ZFeatureR15Migrator-Test",
    "ZFeatureRail",
    "ZFeatureRealWorldCommerce",
    "ZFeatureReport-Anything",
    "ZFeatureRetchecksV2",
    "ZFeatureRibbonRefactor",
    "ZFeatureSecret-Store-Test",
    "ZFeatureShorelines",
    "ZFeatureSignal-Reduce-Copies",
    "ZFeatureSignalConcurrency",
    "ZFeatureSignalDoubleLink",
    "ZFeatureSoundworks-Testing",
    "ZFeatureStrafing",
    "ZFeatureStudioNotifications",
    "ZFeatureSubsystemAtomic",
    "ZFeatureSubsystemHttpClient",
    "ZFeatureSupport_Installer",
    "ZFeatureTelemLife",
    "ZFeatureTeleport_Fix",
    "ZFeatureTest-Build",
    "ZFeatureTestDeploy",
    "ZFeatureTestLength",
    "ZFeatureTest_Rename",
    "ZFeatureThreadNamesWindows",
    "ZFeatureUIEdgeExtension",
    "ZFeatureUpgrade-LZ4",
    "ZFeatureUpgrade-libyuv-Bento4",
    "ZFeatureUse-New-RapidJSON-In-Flag-Loading",
    "ZFeatureVRImmersionMode",
    "ZFeatureVR-Follow-Camera",
    "ZFeatureWin64_Client_Deploy_Test",
    "ZFFlagFCFixDestroyClusters",
    "ZFFPContractOff",
    "ZFileWatcherTesting",
    "ZFixDockPanelCrash",
    "ZFixExtraParserMapEntries",
    "ZFlag",
    "ZFlagEditor",
    "ZFlaggedSplitHashUpdateTestingV2",
    "ZFlexLayoutControl",
    "ZFlexLayoutTest2",
    "ZForceConstructionInCreatable",
    "ZFPM",
    "ZFullCache",
    "ZGraphicsControlGroup",
    "ZGraphicsOptimizationModeScalesHighFlag",
    "ZGraphicsOptimizationModeScalesLowFlag",
    "ZGraphicsOptimizationModeScalesMidFlag",
    "ZGraphicsSurfaceGuiRender3DAdornOpt",
    "ZGuiMixinMigrationChannel",
    "ZHack-Group",
    "ZHarmonyFRMFixRunningAverageTestChannel",
    "ZHarmonyFRMTunableV1Binary",
    "ZHarmonyNewEffectModelOff",
    "ZHarmonyNewEffectModelOff638",
    "ZHarmonyNewEffectModelOn",
    "ZHarmonyNewEffectModelOn637",
    "ZHarmonyNewEffectModelOn638",
    "ZHermeticBuilds",
    "ZHighEnrollmentMetricsTest",
    "ZHighPingTest",
    "ZHumanoidDefaultReplaceUnsafeBlockingLoadInstance",
    "ZHumanoidDescriptionTesting",
    "ZHumanoidPhysics",
    "ZIlya",
    "ZIlyaControl",
    "ZImmersiveAdsDebug",
    "ZIncreaseFileDescriptorLimit",
    "ZInsertServiceReplaceUnsafeBlockingLoadInstance",
    "ZInstanceClusterDeferredMeshRebind",
    "ZInstanceClusterRetryPartInvalidation",
    "ZInstanceGlob",
    "ZInstanceGlob3",
    "ZInstanceGlob-Control",
    "ZIntegration",
    "ZIntegration1",
    "ZIntegration2",
    "ZIntegration3",
    "ZIntegration4",
    "ZIntegration5",
    "ZIntegration6",
    "ZIntegration7",
    "ZIntegration8",
    "ZIntegration9",
    "ZIntegrationTest",
    "ZIntegrationTest1",
    "ZIntegrationTest2",
    "ZIntegrationTest3",
    "ZIntegrationTest4",
    "ZIntegrationTest5",
    "ZJJJ",
    "ZJsonAllocatorChanges",
    "ZKTong-Crash-Hang-Testing",
    "ZKTong-Crash-Hang-Testing-Windows",
    "ZLang",
    "ZLive",
    "ZLive1",
    "ZLive2",
    "ZLive3",
    "ZLiveForBeta",
    "ZLogServiceTest",
    "ZLoom",
    "ZLuaRibbon",
    "ZLuaStartPageSmokeTest",
    "ZLuaTest",
    "ZLuauFileSyncTesting",
    "ZMacArm64",
    "ZManualHookTest",
    "ZManualHookTest5",
    "ZMaterialGenerator",
    "ZMaterialPicker",
    "ZMoreGranularTelemetryProfiling",
    "ZMovementAlpha",
    "ZNativeCodegen2",
    "ZNetworkExTO",
    "ZNetworkInspector",
    "ZNewDocking",
    "ZNew-Grid",
    "ZNew-Grid-Desktop",
    "ZNew-Grid-Test",
    "ZNext",
    "ZNoCommonVerbs627",
    "ZNoRupp",
    "ZOverlapTest",
    "ZP2A-2",
    "ZPackageVersionNotes",
    "ZPathFinding-Improvements",
    "ZPerfRegression",
    "ZPerfVerify",
    "ZPhysicsDebug",
    "ZPhysicsDebugSK",
    "ZProject512-Boost-Remove-Mutex-1",
    "ZProject516-Boost-Remove-Mutex-Network",
    "ZProject587-Split-Remote-File",
    "ZProject588-Split-Remote-File",
    "ZProjectAssetDeliveryV2",
    "ZProjectAutoSetup",
    "ZProjectBackupLiveEditingTest",
    "ZProjectELiu",
    "ZProjectEngineStyling",
    "ZProjectEngine-Styling-Demo",
    "ZProjectEngine-Styling",
    "ZProjectGeometryGenerator",
    "ZProjectImmersiveAdsDebug",
    "ZProjectLiveEditingDemo",
    "ZProjectLiveEditingProperty",
    "ZProjectLiveEditingProperty2",
    "ZProjectLoadingScreen",
    "ZProjectLoadingScreenDeployment",
    "ZProjectLoadingScreen2",
    "ZProjectLuaProxyParts",
    "ZProjectMacProtocolHandlerSwitch",
    "ZProjectMacProtocolHandlerSwitchPublic",
    "ZProjectManageCollaborators3",
    "ZProjectModelsAsPackages",
    "ZProjectMovement-Alpha",
    "ZProjectMovement-Beta",
    "ZProjectNetwork-Transport-Split",
    "ZProjectProxyParts",
    "ZProjectQt5159",
    "ZProjectRCCELiu",
    "ZProjectServer-Authority",
    "ZProjectTelemetry-Test",
    "ZProjectTerrainEditorAlpha",
    "ZProjectTerrainEditorOverhaul",
    "ZProjectTerrainSmoothTool",
    "ZProjectUWPUA",
    "ZProjectUpgradeLibPNG",
    "ZProjectUpdate-WebRTC",
    "ZProjectUpdate-WebRTC-Fetch-Content",
    "ZPublic",
    "ZPublicBetaDogFooding2",
    "ZPublicBetaDogFoodingChris1",
    "ZPublicBetaDogFoodingChris2",
    "ZPublicBetaDogFoodingGeneral1",
    "ZPublicBetaDogFoodingGeneral2",
    "ZPublicBetaDogFoodingGeneral3",
    "ZPublicBetaDogFoodingJeffrey1",
    "ZPublicBetaDogFoodingJeffrey3",
    "ZPublicBetaDogFoodingMichael1",
    "ZPublicBetaDogFoodingMichael2",
    "ZPublicBetaDogFoodingRyan1",
    "ZPublicBetaDogFoodingRyan2",
    "ZPublicBetaDogFoodingRyan3",
    "ZPublicBetaDogFoodingTheo1",
    "ZPublicBetaDogFoodingTheo2",
    "ZPublicBetaDogFoodingsIjun1",
    "ZPublicBetaDogFoodingsIjun2",
    "ZPublicHookTest2",
    "ZPublicHookTest4",
    "ZPublicTestingGUIMemoryReduction",
    "ZPureHookTest",
    "ZPureHookTest2",
    "ZPureHookTest3",
    "ZQt",
    "ZQtCanary",
    "ZQtIntegration",
    "ZQTitanStudio",
    "ZQTitanStudioRelease",
    "ZQTitanStudioReleaseHighDpi",
    "ZQTitanStudioReleaseHighDpiTest",
    "ZQTitanStudioReleaseTest",
    "ZQQIU-0903-ST-1",
    "ZQQIU-0903-ST-3",
    "ZQQIU-Channel-Adoption",
    "ZQQIU-Channel-Adoption-1",
    "ZQQIU-Point-V2-Gating-Logic-V1",
    "ZQQIU-Point-V2-Parity-Check-1",
    "ZQQIU-Point-V2-Parity-Check-2",
    "ZQQIU-Point-V2-Parity-Check-3",
    "ZQQIU-Point-V2-Parity-Check-4",
    "ZQQIU-Send-Points-To-V2",
    "ZQQIU-ST-Common-Device-0",
    "ZQQIU-ST-Common-Device-1",
    "ZQQIU-ST-Point-V2-0812",
    "ZQQIU-Telemetry-Tiger-1",
    "ZQQIU-Telemetry-Tiger-2",
    "ZQQIU-V2Point-Dup-Fix",
    "ZQQIU-V2Point-Dup-Fix1",
    "ZQQIU-V2Point-Verification-Fix1",
    "ZQQIU-V2-Point-ClientCrash-1",
    "ZQQIU-V2-Point-PlaceId",
    "ZQQIU-V2-Point-PlaceId-Client",
    "ZRCCCrashHandlerTest",
    "ZRealTimeSessionTrackingReportingMac2",
    "ZReduceFontAssetIdAllocs",
    "ZRegressionTest1",
    "ZRegressionTest2",
    "ZRegressionTest3",
    "ZRegressionTest4",
    "ZRegressionTest10",
    "ZRegressionTest11",
    "ZRegressionTest12",
    "ZRegressionTest13",
    "ZRegressionTest14",
    "ZRegressionTest15",
    "ZRegressionTest16",
    "ZRegressionTest17",
    "ZRegressionTest18",
    "ZRegressionTest19",
    "ZRegressionTest20",
    "ZRegressionTest21",
    "ZRegressionTest22",
    "ZRegressionTest23",
    "ZRegressionTest24",
    "ZRegressionTest25",
    "ZRegressionTest26",
    "ZRegressionTest27",
    "ZRegressionTest28",
    "ZRegressionTest29",
    "ZReleaseVS2019",
    "ZRemoveExternalTextureRef",
    "ZRemoveExternalTextureRef-Control",
    "ZRemoveMTypesetters",
    "ZRenderInvalidateOpt",
    "ZRenderInvalidateOptTest",
    "ZReportEarlyTelemetryData",
    "ZRobusta",
    "ZRupp",
    "ZRuppDebug",
    "ZSATintTest633",
    "ZScopedRefPtrReleaseBug",
    "ZSecretStoreTest",
    "ZSecretUrlEncodeTest",
    "ZSerializerApiTest",
    "ZSessionTrackingRealTimeReportingMac",
    "ZSessionTrackingRealTimeReportingWindows",
    "ZSimLazyIslandUpdateAndRepack",
    "ZSimpleJSONDeprecateStringDefaultHandler",
    "ZSlowerTerrain",
    "ZSmoothClusterFixes",
    "ZSocialTeam",
    "ZSocial-Presence",
    "ZSplitHashUpdateTesting",
    "ZSplitHashUpdateTestingV2",
    "ZStagedRolloutTest1",
    "ZStagedRolloutTest2",
    "ZStaleFlag",
    "ZStaleFlagCleanup",
    "ZStaleFlagControl",
    "ZStandByPrivate",
    "ZSTIntegration",
    "ZStudioInt1",
    "ZStudioInt2",
    "ZStudioInt3",
    "ZStudioInt4",
    "ZStudioInt5",
    "ZStudioInt6",
    "ZStudioInt7",
    "ZStudioInt8",
    "ZTagPopup",
    "ZTagPopupTest",
    "ZTagTest",
    "ZTaskSchedulerMK3OffAndroid",
    "ZTaskSchedulerMK3OnAndroid",
    "ZTelemetryLarge",
    "ZTelemetrySmall",
    "ZTeleport-Migration",
    "ZTempTestDogFoodSession2A",
    "ZTempTestDogFoodSession2B",
    "ZTempTestEnrollmentSubmit1",
    "ZTempTestEnrollmentSubmit2",
    "ZTempTestEnrollmentSubmit3",
    "ZTempTestEnrollmentSubmit5",
    "ZTempTestEnrollmentSubmit6",
    "ZTempTest4",
    "ZTerrain-Validator",
    "ZTerrain-Validator2",
    "ZTerrainGenerationAlpha",
    "ZTerrainGenerationTest",
    "ZTest1",
    "ZTestChannelService",
    "ZTestChannel",
    "ZTestConnectionPreWarmingMac4",
    "ZTesting",
    "ZTestMac2",
    "ZTestMacShaderAllocation",
    "ZTestPose",
    "ZTextureDedup",
    "ZTextureDedup-Control",
    "ZTheod-Test-003",
    "ZTotalBytesInAllocStats",
    "ZUADF",
    "ZUADF2",
    "ZUADFMeshLODRelease",
    "ZUADFMobile",
    "ZUADFRelease",
    "ZUFCTesting",
    "ZUFCTesting-Flags-On",
    "ZUIDragDetector",
    "ZUnifiedHangMonitor",
    "ZUpdateLayoutTelemetryExperiment",
    "ZUpdateLayoutTelemetryExperiment2",
    "ZUpdateLayoutTelemetryExperiment3",
    "ZUserInfoAcquiredLog",
    "ZUserInfoAcquiredTelemetry",
    "ZVariantPrivate1",
    "ZVariantPrivate2",
    "ZViewportFrameEnvMapSupport",
    "ZVisBugChecks",
    "ZVisBugChecks-619",
    "ZVisBugChecks-620",
    "ZVisualizationModes",
    "ZVNG",
    "ZVoice1485DurationLogger",
    "ZVoiceChatCulling",
    "ZVoiceChatMemoryImprovements",
    "ZVoiceCulling",
    "ZVoiceJoinOperationExtendedTimeout",
    "ZVoiceJoinOperationFix",
    "ZVoiceJoinOperationLuaFixFlagOnly",
    "ZVoiceUpsell",
    "ZVoiceUpsell3",
    "ZVS2019",
    "ZWin64_Client_Test",
    "ZWindowsConnectionPreWarmingTest",
    "ZWinInstallerTelemExperiment",
    "ZWinPlayer64",
    "ZXCode13",
    "ZXCode13Release",
    "ZZTempTestEnrollmentSubmit4",
    "Z1DAFlag",
    "Z1WAFlag",
    "Z2WAFlag",
    "Z3WAFlag",
    "Z4WAFlag",
    "Z16Cookie"
]
use clap::{Arg, ArgGroup, Command};
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use itertools::Itertools;
use prettytable::{cell, row, Table};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rayon::prelude::*;
use regex::Regex;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use retry::{delay::Exponential, retry};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fs;
use std::io::{self, BufRead};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use thiserror::Error;
use tracing::{debug, error, info, warn};
use tracing_subscriber;

/// Custom application error types.
#[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),
    #[error("Configuration error: {0}")]
    Config(String),
    #[error("Invalid argument: {0}")]
    InvalidArg(String),
}

/// Represents information about a valid channel.
#[derive(Debug, Clone, 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,
}

/// Serializable version of ChannelInfo without colored_channel.
#[derive(Debug, Deserialize, Serialize)]
struct ChannelInfoSerializable {
    channel: String,
    roblox_exe: bool,
    deploy_history: bool,
    mac_deploy_history: bool,
    client_settings: bool,
}

/// Holds categorized words extracted from known channels.
#[derive(Debug, Default)]
struct ChannelCategories {
    features: HashSet<String>,
    actions: HashSet<String>,
    modifiers: HashSet<String>,
    endings: HashSet<String>,
    numbers: HashSet<String>,
    tests: HashSet<String>,
    improvements: HashSet<String>,
}

impl ChannelCategories {
    /// Populates categories based on known channel names.
    fn populate_from_known_channels(&mut self, channels: &[&str]) {
        for &channel in channels.iter() {
            self.categorize(channel);
        }
    }

    /// Categorizes a single channel name.
    fn categorize(&mut self, channel: &str) {
        // Skip excluded channels
        if EXCLUDED_CHANNELS.contains(&channel.parse::<String>().unwrap().as_str()) {
            return;
        }

        // Only process channels starting with 'Z'
        if !channel.starts_with('Z') {
            return;
        }

        // Remove the 'Z' prefix
        let core = &channel[1..];

        // Split by non-alphanumeric characters and numerical suffixes
        let re = Regex::new(r"[^a-zA-Z0-9]").unwrap();
        let parts: Vec<&str> = re.split(core).collect();

        // Categorize based on position or keyword patterns
        for (i, part) in parts.iter().enumerate() {
            let part_lower = part.to_lowercase();
            match i {
                0 => {
                    // Assume the first part is a feature
                    self.features.insert(part.to_string());
                }
                1 => {
                    // Assume the second part is an action
                    self.actions.insert(part.to_string());
                }
                2 => {
                    // Assume the third part can be a modifier, number, test, or improvement
                    if part_lower.contains("test") {
                        self.tests.insert(part.to_string());
                    } else if part_lower.contains("improvement") {
                        self.improvements.insert(part.to_string());
                    } else if part_lower.parse::<u32>().is_ok() {
                        self.numbers.insert(part.to_string());
                    } else {
                        self.modifiers.insert(part.to_string());
                    }
                }
                _ => {
                    // Handle additional parts if necessary
                    if part_lower.parse::<u32>().is_ok() {
                        self.numbers.insert(part.to_string());
                    }
                }
            }
        }
    }
}

/// List of excluded channels that should not be processed or generated.
const EXCLUDED_CHANNELS: &[&str] = &["Common", "Ganesh"];

/// Configuration structure to load from JSON.
#[derive(Debug, Deserialize)]
struct Config {
    exclusions: Vec<String>,
    known_channels: Vec<String>,
    patterns: Vec<String>,
}

fn main() -> Result<(), Box<dyn Error>> {
    // Initialize logging with customizable verbosity
    tracing_subscriber::fmt::init();

    // Parse command-line arguments using Clap
    let matches = Command::new("Advanced Roblox Channel Checker")
        .version("3.0")
        .author("Your Name <youremail@example.com>")
        .about("Advanced tool to generate and validate Roblox channels based on wordlist combinations and predefined patterns")
        .arg(
            Arg::new("config")
                .short('f')
                .long("config")
                .value_name("FILE")
                .help("Sets a custom configuration file (JSON)")
                .required(true),
        )
        .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("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")
        )
        .arg(
            Arg::new("max-length")
                .short('m')
                .long("max-length")
                .value_name("NUMBER")
                .help("Maximum number of words in a channel name combination")
                .default_value("4")
        )
        .arg(
            Arg::new("max-combinations")
                .short('x')
                .long("max-combinations")
                .value_name("NUMBER")
                .help("Maximum number of channel name combinations to generate")
        )
        .group(
            ArgGroup::new("input_group")
                .args(&["input"])
                .required(false),
        )
        .get_matches();

    // Load configuration
    let config_path = matches
        .get_one::<String>("config")
        .expect("Configuration file is required");
    let config = load_config(config_path)?;

    // Override exclusions from config if provided as command-line
    let exclusions = matches
        .get_one::<String>("config")
        .map(|_| config.exclusions.clone())
        .unwrap_or_else(|| vec!["Common".to_string(), "Ganesh".to_string()]);

    // 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 and sanitize wordlist
    let mut unique_wordlist: Vec<String> = wordlist
        .into_iter()
        .unique()
        .filter(|word| is_valid_word(word))
        .collect();
    unique_wordlist.sort();

    // Wordlist Augmentation
    if let Some(prefixes_str) = matches.get_one::<String>("prefixes") {
        let prefixes: Vec<&str> = prefixes_str.split(',').map(|s| s.trim()).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(',').map(|s| s.trim()).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().map_err(|_| {
            AppError::InvalidArg(format!(
                "Invalid number for random-strings: {}",
                num_random_str
            ))
        })?;
        augment_wordlist_with_random_strings(&mut unique_wordlist, num_random)?;
    }

    // Remove duplicates created by augmentation
    unique_wordlist = unique_wordlist.into_iter().unique().collect();

    if unique_wordlist.is_empty() {
        error!("Wordlist is empty after processing. Please provide valid words.");
        return Ok(());
    }

    info!("Final wordlist size: {}", unique_wordlist.len());

    // Initialize channel categories based on known channels
    let mut categories = ChannelCategories::default();
    categories.populate_from_known_channels(&config.known_channels.iter().map(|s| s.as_str()).collect::<Vec<&str>>());

    // Generate sanitized combinations
    let max_length = matches
        .get_one::<String>("max-length")
        .unwrap()
        .parse::<usize>()
        .map_err(|_| AppError::InvalidArg("Invalid number for max-length".to_string()))?;
    info!("Generating combinations up to length {}", max_length);

    let max_combinations = matches
        .get_one::<String>("max-combinations")
        .map(|s| s.parse::<usize>())
        .transpose()
        .map_err(|_| AppError::InvalidArg("Invalid number for max-combinations".to_string()))?;

    let patterns = if !config.patterns.is_empty() {
        config.patterns.clone()
    } else {
        vec![
            "Z{Feature}{Action}{Modifier}{Number}".to_string(),
            "Z{Feature}{Action}{Number}".to_string(),
            "Z{Feature}{Action}{Test}".to_string(),
            "Z{Feature}{Action}{Improvement}".to_string(),
            "Z{Feature}{Action}".to_string(), 
        ]
    };

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

    // Configure HTTP client
    let timeout = matches
        .get_one::<String>("timeout")
        .unwrap()
        .parse::<u64>()
        .map_err(|_| AppError::InvalidArg("Invalid number for timeout".to_string()))?;
    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("#>-"),
    );

    // Configure concurrency
    let concurrency = matches
        .get_one::<String>("concurrency")
        .unwrap()
        .parse::<usize>()
        .map_err(|_| AppError::InvalidArg("Invalid number for concurrency".to_string()))?;
    rayon::ThreadPoolBuilder::new()
        .num_threads(concurrency)
        .build_global()
        .expect("Failed to build Rayon thread pool");

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

    // Initialize caching mechanism
    let cache_path = "channel_cache.json";
    let cache = Arc::new(Mutex::new(load_cache(cache_path)?));

    // Shared state for rate limiting
    let rate_limit = Arc::new(Mutex::new(RateLimitState::Ready));

    let start_time = Instant::now();

    // Process combinations in parallel
    let results: Vec<ChannelInfo> = combinations
        .par_iter()
        .filter_map(|combination| {
            // Rate limiting logic
            enforce_rate_limit(&rate_limit, 100)?;

            // Check cache first
            {
                let cache = cache.lock().unwrap();
                if let Some(cached_info) = cache.get(combination) {
                    pb.inc(1);
                    return Some(ChannelInfo {
                        colored_channel: color_code_channel(combination),
                        original_channel: cached_info.channel.clone(),
                        roblox_exe: cached_info.roblox_exe,
                        deploy_history: cached_info.deploy_history,
                        mac_deploy_history: cached_info.mac_deploy_history,
                        client_settings: cached_info.client_settings,
                    });
                }
            }

            // Perform channel check
            let result = check_channel(&client, combination, &categories);
            pb.inc(1);

            // Update cache if channel is valid
            if let Some(ref info) = result {
                let mut cache = cache.lock().unwrap();
                cache.insert(
                    combination.clone(),
                    ChannelInfoSerializable {
                        channel: info.original_channel.clone(),
                        roblox_exe: info.roblox_exe,
                        deploy_history: info.deploy_history,
                        mac_deploy_history: info.mac_deploy_history,
                        client_settings: info.client_settings,
                    },
                );
            }

            result
        })
        .collect();

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

    let processing_time = start_time.elapsed();

    // Display results in table format
    if results.is_empty() {
        info!("No valid channels found.");
    } else {
        display_results_table(&results);
    }

    // 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)?;
    }

    // Save cache
    save_cache(&cache.lock().unwrap(), cache_path)?;

    // Generate and display report
    generate_and_display_report(&results, combinations.len(), processing_time)?;

    Ok(())
}

/// Enum representing rate limiting state.
enum RateLimitState {
    Ready,
    Waiting(Instant, Duration),
}

/// Enforces rate limiting by allowing a maximum number of requests within a time window.
fn enforce_rate_limit(rate_limit: &Arc<Mutex<RateLimitState>>, max_requests: usize) -> Option<()> {
    let mut rate = rate_limit.lock().unwrap();
    match *rate {
        RateLimitState::Ready => {
            // Start tracking
            *rate = RateLimitState::Waiting(Instant::now(), Duration::from_secs(60));
            Some(())
        }
        RateLimitState::Waiting(start, duration) => {
            if start.elapsed() >= duration {
                // Reset the rate limiter
                *rate = RateLimitState::Waiting(Instant::now(), duration);
                Some(())
            } else {
                // Wait until the rate limiter resets
                None
            }
        }
    }
}

/// Loads cached channel checks from a JSON file.
fn load_cache(path: &str) -> Result<HashMap<String, ChannelInfoSerializable>, AppError> {
    if Path::new(path).exists() {
        let content = fs::read_to_string(path)?;
        let cache: HashMap<String, ChannelInfoSerializable> = serde_json::from_str(&content)?;
        Ok(cache)
    } else {
        Ok(HashMap::new())
    }
}

/// Saves the cache of channel checks to a JSON file.
fn save_cache(cache: &HashMap<String, ChannelInfoSerializable>, path: &str) -> Result<(), AppError> {
    let json = serde_json::to_string_pretty(cache)?;
    fs::write(path, json)?;
    Ok(())
}

/// Loads configuration from a JSON file.
fn load_config<P: AsRef<Path>>(path: P) -> Result<Config, AppError> {
    let content = fs::read_to_string(path)?;
    let config: Config = serde_json::from_str(&content)?;
    Ok(config)
}

/// Reads wordlist from a specified file.
fn read_wordlist_from_file<P: AsRef<Path>>(path: P) -> Result<Vec<String>, AppError> {
    let content = fs::read_to_string(path)?;
    let wordlist = content
        .lines()
        .map(|line| line.trim().to_string())
        .filter(|line| !line.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()
        .map_while(Result::ok)
        .map(|line| line.trim().to_string())
        .filter(|line| !line.is_empty())
        .collect();
    Ok(wordlist)
}

/// Checks if a word is valid (e.g., does not contain invalid characters).
fn is_valid_word(word: &str) -> bool {
    let invalid_pattern = Regex::new(r"[^a-zA-Z0-9]").unwrap();
    if invalid_pattern.is_match(word) {
        warn!("Invalid word detected and skipped: {}", word);
        false
    } else {
        true
    }
}

/// Generates all unique combinations based on patterns up to a specified length,
/// avoiding duplicates and adhering to category constraints.
fn generate_combinations(
    wordlist: &[String],
    patterns: &[String],
    max_length: usize,
    max_combinations: Option<usize>,
) -> HashSet<String> {
    let mut combinations = HashSet::new();
    let mut rng = thread_rng();

    for pattern in patterns {
        let regex = Regex::new(r"\{(\w+)\}").unwrap();
        let placeholders: Vec<&str> = regex
            .captures_iter(pattern)
            .map(|cap| cap.get(1).unwrap().as_str())
            .collect();

        // Generate mappings for placeholders
        let mappings: Vec<Vec<String>> = placeholders
            .iter()
            .map(|&placeholder| match placeholder.to_lowercase().as_str() {
                "feature" => wordlist
                    .iter()
                    .filter(|w| REGEX_FEATURE.is_match(w))
                    .cloned()
                    .collect(),
                "action" => wordlist
                    .iter()
                    .filter(|w| REGEX_ACTION.is_match(w))
                    .cloned()
                    .collect(),
                "modifier" => wordlist
                    .iter()
                    .filter(|w| REGEX_MODIFIER.is_match(w))
                    .cloned()
                    .collect(),
                "number" => wordlist
                    .iter()
                    .filter(|w| w.parse::<u32>().is_ok())
                    .cloned()
                    .collect(),
                "test" => wordlist
                    .iter()
                    .filter(|w| REGEX_TEST.is_match(w))
                    .cloned()
                    .collect(),
                "improvement" => wordlist
                    .iter()
                    .filter(|w| REGEX_IMPROVEMENT.is_match(w))
                    .cloned()
                    .collect(),
                _ => vec![],
            })
            .collect();

        // Generate all combinations for the current pattern
        let all_combos = mappings.iter().multi_cartesian_product();

        for combo in all_combos {
            // Skip if any part is empty
            if combo.iter().any(|word| word.is_empty()) {
                continue;
            }

            // Concatenate parts based on the pattern
            let channel = pattern
                .as_str()
                .replace("{Feature}", &combo[0])
                .replace("{Action}", &combo[1])
                .replace("{Modifier}", if placeholders.contains(&"Modifier") { &combo[2] } else { "" })
                .replace("{Number}", if placeholders.contains(&"Number") { &combo[placeholders.iter().position(|&p| p == "Number").unwrap()] } else { "" })
                .replace("{Test}", if placeholders.contains(&"Test") { &combo[placeholders.iter().position(|&p| p == "Test").unwrap()] } else { "" })
                .replace("{Improvement}", if placeholders.contains(&"Improvement") { &combo[placeholders.iter().position(|&p| p == "Improvement").unwrap()] } else { "" })
                .trim_end_matches('_') // Remove trailing underscores if any
                .to_string();

            // Avoid consecutive duplicates and enforce max_length
            if is_valid_combination(&channel, max_length) {
                combinations.insert(channel);
                // Respect max_combinations limit if set
                if let Some(max) = max_combinations {
                    if combinations.len() >= max {
                        return combinations;
                    }
                }
            }
        }
    }

    combinations
}

/// Validates the combination based on length and non-repeating consecutive words.
fn is_valid_combination(channel: &str, max_length: usize) -> bool {
    if channel.len() > (max_length * 10) { // Assuming average word length ~10
        return false;
    }

    // Split by uppercase letters to get words
    let words: Vec<&str> = Regex::new(r"(?=[A-Z])").unwrap().split(channel).collect();
    if words.len() > max_length {
        return false;
    }

    // Check for consecutive duplicates
    for window in words.windows(2) {
        if window[0].eq_ignore_ascii_case(window[1]) {
            return false;
        }
    }

    true
}

lazy_static::lazy_static! {
    static ref REGEX_FEATURE: Regex = Regex::new(r"^(feature|bugfix|hotfix|project|release|test)$").unwrap();
    static ref REGEX_ACTION: Regex = Regex::new(r"^(deploy|integrate|release|test|check|monitor)$").unwrap();
    static ref REGEX_MODIFIER: Regex = Regex::new(r"^(alpha|beta|prod|test|canary)$").unwrap();
    static ref REGEX_TEST: Regex = Regex::new(r"^(test|testing|experiment)$").unwrap();
    static ref REGEX_IMPROVEMENT: Regex = Regex::new(r"^(improvement|enhancement)$").unwrap();
}

/// Augments the wordlist with given 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() {
            let new_word = format!("{}{}", prefix, word);
            if is_valid_word(&new_word) {
                new_words.push(new_word);
            } else {
                warn!("Invalid prefix combination skipped: {}", new_word);
            }
        }
    }
    wordlist.append(&mut new_words);
}

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() {
            let new_word = format!("{}{}", word, suffix);
            if is_valid_word(&new_word) {
                new_words.push(new_word);
            } else {
                warn!("Invalid suffix combination skipped: {}", new_word);
            }
        }
    }
    wordlist.append(&mut new_words);
}

/// Augments the wordlist with a specified number of random alphanumeric strings.
fn augment_wordlist_with_random_strings(wordlist: &mut Vec<String>, num_strings: usize) -> Result<(), AppError> {
    let rng = thread_rng();
    for _ in 0..num_strings {
        let random_string: String = rng.clone()
            .sample_iter(&Alphanumeric)
            .take(8) // Length of random string
            .map(char::from)
            .collect();
        wordlist.push(random_string);
    }
    Ok(())
}


/// Checks a single channel and returns ChannelInfo if valid.
fn check_channel(
    client: &Client,
    channel_name: &str,
    categories: &ChannelCategories,
) -> Option<ChannelInfo> {
    // Advanced validation based on categories
    if !validate_channel_format(channel_name, categories) {
        return None;
    }

    let base_url = format!("http://setup.rbxcdn.com/channel/zfeature{}", 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/zfeature{}",
                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
                            let body = response.text().unwrap_or_default();
                            Ok(!body.contains(r#"{"applicationSettings":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
    }
}

/// Validates the channel format based on categorized words.
fn validate_channel_format(channel: &str, categories: &ChannelCategories) -> bool {
    // Remove 'Z' prefix
    let core = &channel[1..];

    // Split by uppercase letters to get words
    let words: Vec<&str> = Regex::new(r"(?=[A-Z])").unwrap().split(core).collect();

    if words.is_empty() {
        warn!("Channel '{}' has no parts after 'Z' prefix.", channel);
        return false;
    }

    // Basic pattern checks based on categories
    if words.len() < 2 {
        warn!("Channel '{}' does not meet minimum parts requirement.", channel);
        return false;
    }

    // Check feature
    if !categories.features.contains(&words[0].to_string()) {
        warn!(
            "Channel '{}' has an unknown feature '{}'.",
            channel, words[0]
        );
        return false;
    }

    // Check action
    if !categories.actions.contains(&words[1].to_string()) {
        warn!(
            "Channel '{}' has an unknown action '{}'.",
            channel, words[1]
        );
        return false;
    }

    // Check modifiers, numbers, tests, or improvements
    for &part in &words[2..] {
        let part_lower = part.to_lowercase();
        if !(categories.modifiers.contains(&part.to_string())
            || categories.numbers.contains(&part.to_string())
            || categories.tests.contains(&part.to_string())
            || categories.improvements.contains(&part.to_string()))
        {
            warn!(
                "Channel '{}' has an unknown or invalid part '{}'.",
                channel, part
            );
            return false;
        }
    }

    true
}

/// Applies color coding to channel names based on keywords.
fn color_code_channel(channel: &str) -> ColoredString {
    let channel_lower = channel.to_lowercase();
    if channel_lower.contains("feature") {
        channel.bold().blue()
    } else if channel_lower.contains("bugfix") || channel_lower.contains("hotfix") {
        channel.bold().yellow()
    } else if channel.len() <= 8 {
        channel.cyan()
    } else {
        channel.red()
    }
}

/// Displays the results in a formatted table.
fn display_results_table(results: &[ChannelInfo]) {
    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,
            if info.roblox_exe { "✅".green() } else { "❌".red() },
            if info.deploy_history { "✅".green() } else { "❌".red() },
            if info.mac_deploy_history { "✅".green() } else { "❌".red() },
            if info.client_settings { "✅".green() } else { "❌".red() },
        ]);
    }
    table.printstd();
}

/// Saves results to a JSON file.
fn save_results_to_file(results: &[ChannelInfo], path: &str) -> Result<(), AppError> {
    // Serialize results excluding colored_channel
    let serializable_results: Vec<ChannelInfoSerializable> = results
        .iter()
        .map(|info| ChannelInfoSerializable {
            channel: info.original_channel.clone(),
            roblox_exe: info.roblox_exe,
            deploy_history: info.deploy_history,
            mac_deploy_history: info.mac_deploy_history,
            client_settings: info.client_settings,
        })
        .collect();

    let json = serde_json::to_string_pretty(&serializable_results)?;
    fs::write(path, json)?;
    Ok(())
}

/// Generates and displays a comprehensive report.
fn generate_and_display_report(
    results: &[ChannelInfo],
    total_checked: usize,
    processing_time: Duration,
) -> Result<(), AppError> {
    let valid = results.len();
    let invalid = total_checked - valid;

    // Breakdown by endpoint
    let mut breakdown: HashMap<&str, usize> = HashMap::new();
    for info in results {
        if info.roblox_exe {
            *breakdown.entry("Roblox.exe").or_insert(0) += 1;
        }
        if info.deploy_history {
            *breakdown.entry("DeployHistory.txt").or_insert(0) += 1;
        }
        if info.mac_deploy_history {
            *breakdown.entry("Mac DeployHistory.txt").or_insert(0) += 1;
        }
        if info.client_settings {
            *breakdown.entry("ClientSettings").or_insert(0) += 1;
        }
    }

    // Display report
    println!("\n{}", "=== Comprehensive Report ===".bold().underline());
    println!("Total Channels Checked : {}", total_checked);
    println!("Valid Channels Found  : {}", valid);
    println!("Invalid Channels Found: {}", invalid);
    println!("Processing Time       : {:.2?}", processing_time);
    println!("Breakdown by Endpoint :");

    for (endpoint, count) in breakdown.iter() {
        println!(" - {}: {}", endpoint.bold(), count);
    }

    Ok(())
}