// src/scraper/yahoo.rs - WITH COOKIE AND CRUMB AUTHENTICATION + CLIENT REPLACEMENT
use super::docker_vpn_proxy::DockerVpnProxyPool;
use crate::config::Config;
use crate::util::logger;
use anyhow::{Context, Result, anyhow};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, Semaphore, RwLock};
use tokio::time::{sleep, timeout};
use reqwest::{Client, ClientBuilder};
use reqwest::Proxy as ReqwestProxy;
use std::result::Result::Ok;

// Yahoo API Endpoints
const YAHOO_QUOTE_SUMMARY: &str = "https://query2.finance.yahoo.com/v10/finance/quoteSummary/";
const YAHOO_CHART_DATA: &str = "https://query2.finance.yahoo.com/v8/finance/chart/";
const YAHOO_OPTION_DATA: &str = "https://query1.finance.yahoo.com/v7/finance/options/";
const YAHOO_SEARCH: &str = "https://query2.finance.yahoo.com/v1/finance/search";
const _YAHOO_HOMEPAGE: &str = "https://finance.yahoo.com";
const YAHOO_CRUMB_URL: &str = "https://query2.finance.yahoo.com/v1/test/getcrumb";

// QuoteSummary Modules
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QuoteSummaryModule {
    AssetProfile,
    SummaryProfile,
    SummaryDetail,
    EsgScores,
    Price,
    IncomeStatementHistory,
    IncomeStatementHistoryQuarterly,
    BalanceSheetHistory,
    BalanceSheetHistoryQuarterly,
    CashflowStatementHistory,
    CashflowStatementHistoryQuarterly,
    DefaultKeyStatistics,
    FinancialData,
    CalendarEvents,
    SecFilings,
    RecommendationTrend,
    UpgradeDowngradeHistory,
    InstitutionOwnership,
    FundOwnership,
    MajorDirectHolders,
    MajorHoldersBreakdown,
    InsiderTransactions,
    InsiderHolders,
    NetSharePurchaseActivity,
    Earnings,
    EarningsHistory,
    EarningsTrend,
    IndustryTrend,
    IndexTrend,
    SectorTrend,
}

impl QuoteSummaryModule {
    fn as_str(&self) -> &'static str {
        match self {
            Self::AssetProfile => "assetProfile",
            Self::SummaryProfile => "summaryProfile",
            Self::SummaryDetail => "summaryDetail",
            Self::EsgScores => "esgScores",
            Self::Price => "price",
            Self::IncomeStatementHistory => "incomeStatementHistory",
            Self::IncomeStatementHistoryQuarterly => "incomeStatementHistoryQuarterly",
            Self::BalanceSheetHistory => "balanceSheetHistory",
            Self::BalanceSheetHistoryQuarterly => "balanceSheetHistoryQuarterly",
            Self::CashflowStatementHistory => "cashflowStatementHistory",
            Self::CashflowStatementHistoryQuarterly => "cashflowStatementHistoryQuarterly",
            Self::DefaultKeyStatistics => "defaultKeyStatistics",
            Self::FinancialData => "financialData",
            Self::CalendarEvents => "calendarEvents",
            Self::SecFilings => "secFilings",
            Self::RecommendationTrend => "recommendationTrend",
            Self::UpgradeDowngradeHistory => "upgradeDowngradeHistory",
            Self::InstitutionOwnership => "institutionOwnership",
            Self::FundOwnership => "fundOwnership",
            Self::MajorDirectHolders => "majorDirectHolders",
            Self::MajorHoldersBreakdown => "majorHoldersBreakdown",
            Self::InsiderTransactions => "insiderTransactions",
            Self::InsiderHolders => "insiderHolders",
            Self::NetSharePurchaseActivity => "netSharePurchaseActivity",
            Self::Earnings => "earnings",
            Self::EarningsHistory => "earningsHistory",
            Self::EarningsTrend => "earningsTrend",
            Self::IndustryTrend => "industryTrend",
            Self::IndexTrend => "indexTrend",
            Self::SectorTrend => "sectorTrend",
        }
    }

    pub fn all_modules() -> Vec<Self> {
        vec![
            Self::AssetProfile,
            Self::SummaryProfile,
            Self::SummaryDetail,
            Self::EsgScores,
            Self::Price,
            Self::IncomeStatementHistory,
            Self::IncomeStatementHistoryQuarterly,
            Self::BalanceSheetHistory,
            Self::BalanceSheetHistoryQuarterly,
            Self::CashflowStatementHistory,
            Self::CashflowStatementHistoryQuarterly,
            Self::DefaultKeyStatistics,
            Self::FinancialData,
            Self::CalendarEvents,
            Self::SecFilings,
            Self::RecommendationTrend,
            Self::UpgradeDowngradeHistory,
            Self::InstitutionOwnership,
            Self::FundOwnership,
            Self::MajorDirectHolders,
            Self::MajorHoldersBreakdown,
            Self::InsiderTransactions,
            Self::InsiderHolders,
            Self::NetSharePurchaseActivity,
            Self::Earnings,
            Self::EarningsHistory,
            Self::EarningsTrend,
            Self::IndustryTrend,
            Self::IndexTrend,
            Self::SectorTrend,
        ]
    }

    pub fn financial_modules() -> Vec<Self> {
        vec![
            Self::IncomeStatementHistory,
            Self::IncomeStatementHistoryQuarterly,
            Self::BalanceSheetHistory,
            Self::BalanceSheetHistoryQuarterly,
            Self::CashflowStatementHistory,
            Self::CashflowStatementHistoryQuarterly,
        ]
    }

    pub fn ownership_modules() -> Vec<Self> {
        vec![
            Self::InstitutionOwnership,
            Self::FundOwnership,
            Self::MajorDirectHolders,
            Self::MajorHoldersBreakdown,
            Self::InsiderTransactions,
            Self::InsiderHolders,
            Self::NetSharePurchaseActivity,
        ]
    }

    pub fn event_modules() -> Vec<Self> {
        vec![
            Self::CalendarEvents,
            Self::SecFilings,
            Self::Earnings,
            Self::EarningsHistory,
            Self::UpgradeDowngradeHistory,
        ]
    }

    pub fn sentiment_modules() -> Vec<Self> {
        vec![
            Self::EarningsTrend,
            Self::IndustryTrend,
            Self::IndexTrend,
            Self::SectorTrend,
            Self::RecommendationTrend,
            Self::UpgradeDowngradeHistory,
        ]
    }

    pub fn core_modules() -> Vec<Self> {
        vec![
            Self::Price,
            Self::SummaryDetail,
            Self::DefaultKeyStatistics,
            Self::FinancialData,
        ]
    }
}

// Response structures
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuoteSummary {
    pub symbol: String,
    pub modules: HashMap<String, Value>,
    pub timestamp: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChartData {
    pub symbol: String,
    pub quotes: Vec<Quote>,
    pub timestamp: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Quote {
    pub timestamp: i64,
    pub open: Option<f64>,
    pub high: Option<f64>,
    pub low: Option<f64>,
    pub close: Option<f64>,
    pub volume: Option<u64>,
    pub adjusted_close: Option<f64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionData {
    pub symbol: String,
    pub expiration_dates: Vec<i64>,
    pub strikes: Vec<f64>,
    pub option: Vec<OptionChain>,
    pub timestamp: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionChain {
    pub expiration_date: i64,
    pub calls: Vec<OptionContract>,
    pub puts: Vec<OptionContract>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionContract {
    pub strike: f64,
    pub last_price: Option<f64>,
    pub bid: Option<f64>,
    pub ask: Option<f64>,
    pub volume: Option<u64>,
    pub open_interest: Option<u64>,
    pub implied_volatility: Option<f64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
    pub symbol: String,
    pub name: String,
    pub exchange: String,
    pub asset_type: String,
}

/// YahooClient - Individual client with unique proxy and cookie/crumb management
pub struct YahooClient {
    client: Client,
    client_id: usize,
    proxy_url: String,
    task_count: Arc<Mutex<usize>>,
    max_tasks_per_client: usize,
    request_count: Arc<Mutex<u32>>,
    monitoring: Option<crate::monitoring::MonitoringHandle>,
    crumb: Arc<Mutex<Option<String>>>,  // Cached crumb
    crumb_last_refresh: Arc<Mutex<Option<Instant>>>,  // Track when crumb was last refreshed
    consecutive_failures: Arc<AtomicUsize>,
    max_consecutive_failures: usize,  // Default: 3
    is_marked_for_replacement: Arc<AtomicBool>,
}

impl YahooClient {
    /// Create a new YahooClient with mandatory proxy assignment and cookie/crumb authentication
    pub async fn new(
        client_id: usize,
        proxy_url: String,
        max_tasks_per_client: usize,
        monitoring: Option<crate::monitoring::MonitoringHandle>,
    ) -> Result<Self> {
        logger::log_info(&format!(
            "Creating YahooClient[{}] with proxy: {}",
            client_id, proxy_url
        )).await;

        // Configure reqwest client with SOCKS5 proxy and cookie support
        let proxy = ReqwestProxy::all(&proxy_url)
            .context("Failed to create proxy configuration")?;

        let client = ClientBuilder::new()
            .proxy(proxy)
            .timeout(Duration::from_secs(30))  // CHANGED: Reduced from 90s to 30s
            .connect_timeout(Duration::from_secs(10))  // CHANGED: Reduced from 30s to 10s
            .pool_max_idle_per_host(2)
            .pool_idle_timeout(Duration::from_secs(60))
            .cookie_store(true)
            .user_agent(Self::random_user_agent())
            .default_headers({
                let mut headers = reqwest::header::HeaderMap::new();
                headers.insert("Accept", "application/json, text/plain, */*".parse().unwrap());
                headers.insert("Accept-Language", "en-US,en;q=0.9".parse().unwrap());
                headers.insert("Accept-Encoding", "gzip, deflate, br".parse().unwrap());
                headers.insert("Connection", "keep-alive".parse().unwrap());
                headers.insert("Referer", "https://finance.yahoo.com/".parse().unwrap());
                headers.insert("Origin", "https://finance.yahoo.com".parse().unwrap());
                headers.insert("Sec-Fetch-Dest", "empty".parse().unwrap());
                headers.insert("Sec-Fetch-Mode", "cors".parse().unwrap());
                headers.insert("Sec-Fetch-Site", "same-site".parse().unwrap());
                headers
            })
            .build()
            .context("Failed to build HTTP client")?;

        let yahoo_client = Self {
            client,
            client_id,
            proxy_url,
            task_count: Arc::new(Mutex::new(0)),
            max_tasks_per_client,
            request_count: Arc::new(Mutex::new(0)),
            monitoring,
            crumb: Arc::new(Mutex::new(None)),
            crumb_last_refresh: Arc::new(Mutex::new(None)),
            consecutive_failures: Arc::new(AtomicUsize::new(0)),
            max_consecutive_failures: 3,
            is_marked_for_replacement: Arc::new(AtomicBool::new(false)),
        };

        // NEW: Quick proxy health check before crumb initialization
        logger::log_info(&format!(
            "  YahooClient[{}] testing proxy connectivity...",
            client_id
        )).await;
        
        match timeout(
            Duration::from_secs(5),
            yahoo_client.client.get("https://finance.yahoo.com").send()
        ).await {
            Ok(Ok(_)) => {
                logger::log_info(&format!(
                    "  ✓ YahooClient[{}] proxy is responsive",
                    client_id
                )).await;
            }
            Ok(Err(e)) => {
                return Err(anyhow!(
                    "Proxy connection failed for YahooClient[{}]: {}",
                    client_id, e
                ));
            }
            Err(_) => {
                return Err(anyhow!(
                    "Proxy connection timeout for YahooClient[{}]",
                    client_id
                ));
            }
        }

        // Initialize crumb with timeout wrapper
        match timeout(
            Duration::from_secs(30),  // NEW: 30 second timeout for entire initialization
            yahoo_client.try_initialize_crumb(&yahoo_client)
        ).await {
            Ok(Ok(())) => {
                logger::log_info(&format!(
                    "  ✓ YahooClient[{}] initialized (max_tasks: {})",
                    client_id, max_tasks_per_client
                )).await;
                Ok(yahoo_client)
            }
            Ok(Err(e)) => {
                Err(anyhow!(
                    "Failed to initialize YahooClient[{}]: {}",
                    client_id, e
                ))
            }
            Err(_) => {
                Err(anyhow!(
                    "Timeout initializing YahooClient[{}] (>30s)",
                    client_id
                ))
            }
        }
    }

    async fn try_initialize_crumb(&self, client: &YahooClient) -> Result<()> {        
        match client.initialize_crumb().await {
            Ok(()) => return Ok(()),
            Err(e) => {
                let error_str: String = e.to_string();
                
                // If it's a permanent error, don't retry
                if error_str.contains("Invalid Cookie") {
                    logger::log_error(&format!(
                        "  YahooClient[{}] permanent cookie error, not retrying",
                        self.client_id
                    )).await;
                }
                
                logger::log_info(&format!(
                    "  YahooClient[{}] crumb attempt failed {}",
                    self.client_id, error_str
                )).await;

                Err(anyhow!("Failed fetching crumb: {}", error_str))
            }
        }
    }

    // Update the crumb fetching in the initialize_crumb function
    async fn initialize_crumb(&self) -> Result<()> {
        logger::log_info(&format!(
            "  YahooClient[{}] fetching cookies and crumb...",
            self.client_id
        )).await;

        // Step 1: Establish session with timeout
        let yahoo_domains = [
            "https://finance.yahoo.com",
            "https://www.yahoo.com",
            "https://login.yahoo.com",
        ];
        
        for domain in yahoo_domains.iter() {
            // NEW: Add timeout and better error handling
            match timeout(
                Duration::from_secs(5),
                self.client
                    .get(*domain)
                    .header("User-Agent", Self::random_user_agent())
                    .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
                    .header("Accept-Language", "en-US,en;q=0.9")
                    .header("Accept-Encoding", "gzip, deflate")
                    .header("DNT", "1")
                    .header("Connection", "keep-alive")
                    .header("Upgrade-Insecure-Requests", "1")
                    .send()
            ).await {
                Ok(Ok(response)) if response.status().is_success() => {
                    logger::log_info(&format!(
                        "  ✓ YahooClient[{}] session established via {}",
                        self.client_id, domain
                    )).await;
                }
                Ok(Ok(response)) => {
                    logger::log_warn(&format!(
                        "  ⚠ YahooClient[{}] session request to {} returned {}",
                        self.client_id, domain, response.status()
                    )).await;
                }
                Ok(Err(e)) => {
                    logger::log_warn(&format!(
                        "  ⚠ YahooClient[{}] failed to connect to {}: {}",
                        self.client_id, domain, e
                    )).await;
                }
                Err(_) => {
                    logger::log_warn(&format!(
                        "  ⚠ YahooClient[{}] timeout connecting to {}",
                        self.client_id, domain
                    )).await;
                }
            }
            
            sleep(Duration::from_millis(500)).await;
        }
        
        // Step 2: Fetch crumb with timeout
        logger::log_info(&format!(
            "  YahooClient[{}] requesting crumb...",
            self.client_id
        )).await;
        
        let crumb_result = timeout(
            Duration::from_secs(30),  // NEW: 30 second timeout for crumb fetch
            self.client
                .get(YAHOO_CRUMB_URL)
                .header("User-Agent", Self::random_user_agent())
                .header("Accept", "*/*")
                .header("Accept-Language", "en-US,en;q=0.9")
                .header("Accept-Encoding", "gzip, deflate, br")
                .header("Referer", "https://finance.yahoo.com/")
                .header("Origin", "https://finance.yahoo.com")
                .header("Sec-Fetch-Dest", "empty")
                .header("Sec-Fetch-Mode", "cors")
                .header("Sec-Fetch-Site", "same-site")
                .header("Pragma", "no-cache")
                .header("Cache-Control", "no-cache")
                .send()
        ).await;
        
        match crumb_result {
            Ok(Ok(response)) if response.status().is_success() => {
                let crumb_text = response.text().await?;
                let crumb = crumb_text.trim().to_string();
                
                if !crumb.is_empty() && crumb != "null" && !crumb.contains("<!--") {
                    logger::log_info(&format!(
                        "  ✓ YahooClient[{}] obtained crumb ({}...)",
                        self.client_id,
                        &crumb[..crumb.len().min(8)]
                    )).await;
                    
                    // Store crumb and refresh time
                    let mut crumb_guard = self.crumb.lock().await;
                    *crumb_guard = Some(crumb);
                    
                    let mut refresh_guard = self.crumb_last_refresh.lock().await;
                    *refresh_guard = Some(Instant::now());
                    
                    return Ok(());
                } else {
                    return Err(anyhow!(
                        "YahooClient[{}] got invalid crumb format: '{}'",
                        self.client_id, crumb
                    ));
                }
            }
            Ok(Ok(response)) => {
                let status = response.status();
                let error_text = response.text().await.unwrap_or_default();
                return Err(anyhow!(
                    "YahooClient[{}] crumb fetch failed: HTTP {} - {}",
                    self.client_id, status, &error_text[..error_text.len().min(100)]
                ));
            }
            Ok(Err(e)) => {
                return Err(anyhow!(
                    "YahooClient[{}] crumb connection error: {}",
                    self.client_id, e
                ));
            }
            Err(_) => {
                return Err(anyhow!(
                    "YahooClient[{}] crumb fetch timeout (>10s)",
                    self.client_id
                ));
            }
        }
    }

    /// Get the current crumb (refresh if needed)
    async fn get_crumb(&self) -> Result<String> {
        // Check if crumb needs refresh (older than 30 minutes)
        let refresh_guard = self.crumb_last_refresh.lock().await;
        let should_refresh = match *refresh_guard {
            Some(last_refresh) => last_refresh.elapsed() > Duration::from_secs(1800),
            None => true,
        };
        drop(refresh_guard);

        if should_refresh {
            logger::log_info(&format!(
                "  YahooClient[{}] crumb expired, refreshing...",
                self.client_id
            )).await;
            self.initialize_crumb().await?;
        }

        let crumb_guard = self.crumb.lock().await;
        crumb_guard.clone().ok_or_else(|| anyhow!("Crumb not initialized"))
    }

    /// Refresh crumb if request failed with 401
    async fn handle_unauthorized_error(&self, error_text: &str) -> Result<()> {
        if error_text.contains("401") 
            || error_text.contains("Unauthorized") 
            || error_text.contains("Invalid Crumb")
            || error_text.contains("invalid crumb") {
            
            logger::log_warn(&format!(
                "  ⚠ YahooClient[{}] detected invalid crumb, refreshing...",
                self.client_id
            )).await;
            
            self.initialize_crumb().await?;
            
            // Small delay to let new session establish
            sleep(Duration::from_millis(200)).await;
        }
        Ok(())
    }

    /// Check if client is healthy (not marked for replacement and has reasonable failure count)
    pub fn is_healthy(&self) -> bool {
        let failures = self.consecutive_failures.load(Ordering::Relaxed);
        !self.is_marked_for_replacement.load(Ordering::Relaxed) && failures < self.max_consecutive_failures
    }
    
    /// Check if client is marked for replacement
    pub fn is_marked_for_replacement(&self) -> bool {
        self.is_marked_for_replacement.load(Ordering::Relaxed)
    }
    
    /// Record a success - reset failure count
    pub fn record_success(&self) {
        self.consecutive_failures.store(0, Ordering::Relaxed);
        self.is_marked_for_replacement.store(false, Ordering::Relaxed);
    }
    
    /// Record a failure - return true if client should be replaced
    pub async fn record_failure(&self, error_str: &str) -> bool {
        // Don't count 404 as failures - these are expected when data doesn't exist
        if error_str.contains("404") || error_str.contains("Not Found") {
            return false; // Don't mark for replacement
        }
        if error_str.contains("500") || error_str.contains("Not Found") {
            return false; // Don't mark for replacement
        }

        let old_failures = self.consecutive_failures.fetch_add(1, Ordering::Relaxed);
        let new_failures = old_failures + 1;
        
        // Check if we should mark for replacement
        let should_replace = new_failures >= self.max_consecutive_failures;
        
        if should_replace {
            logger::log_warn(&format!(
                "  🚨 YahooClient[{}] marked for replacement ({} consecutive failures, last error: {})",
                self.client_id, new_failures, error_str
            )).await;
            self.is_marked_for_replacement.store(true, Ordering::Relaxed);
        } else {
            logger::log_warn(&format!(
                "  ⚠ YahooClient[{}] failure {}/{}: {}",
                self.client_id, new_failures, self.max_consecutive_failures, error_str
            )).await;
        }
        
        should_replace
    }
    
    /// Force mark for replacement (for external use)
    pub fn mark_for_replacement(&self) {
        self.is_marked_for_replacement.store(true, Ordering::Relaxed);
    }
    
    /// Get current failure count
    pub fn get_failure_count(&self) -> usize {
        self.consecutive_failures.load(Ordering::Relaxed)
    }

    /// Get client ID
    pub fn get_client_id(&self) -> usize {
        self.client_id
    }

    /// Get proxy URL
    pub fn get_proxy_url(&self) -> &str {
        &self.proxy_url
    }

    /// Check if client is available for new tasks (respects task limit)
    pub async fn is_available(&self) -> bool {
        if self.max_tasks_per_client == 0 {
            return true; // No limit
        }
        let count = self.task_count.lock().await;
        *count < self.max_tasks_per_client
    }

    /// Get remaining task capacity
    pub async fn tasks_remaining(&self) -> usize {
        if self.max_tasks_per_client == 0 {
            return usize::MAX;
        }
        let count = self.task_count.lock().await;
        self.max_tasks_per_client.saturating_sub(*count)
    }

    /// Increment task count (called when task starts)
    async fn increment_task_count(&self) {
        let mut count = self.task_count.lock().await;
        *count += 1;
    }

    /// Decrement task count (called when task completes)
    async fn decrement_task_count(&self) {
        let mut count = self.task_count.lock().await;
        *count = count.saturating_sub(1);
    }

    /// Increment request count
    async fn increment_request_count(&self) {
        let mut count = self.request_count.lock().await;
        *count += 1;
    }

    /// Generate random user agent
    fn random_user_agent() -> String {
        let user_agents = [
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0",
        ];
        
        let idx = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs() as usize % user_agents.len();
        
        user_agents[idx].to_string()
    }

    /// Get quote summary for a symbol
    pub async fn get_quote_summary(
        &self,
        symbol: &str,
        modules: &[QuoteSummaryModule],
    ) -> Result<QuoteSummary> {
        self.increment_request_count().await;

        let modules_str: Vec<&str> = modules.iter().map(|m| m.as_str()).collect();
        
        // Try request with current crumb
        let result = self.get_quote_summary_internal(symbol, &modules_str).await;
        
        // If unauthorized, refresh crumb and retry once
        if let Err(ref e) = result {
            let error_msg = e.to_string();
            if error_msg.contains("401") || error_msg.contains("Unauthorized") {
                self.handle_unauthorized_error(&error_msg).await?;
                
                // Retry with fresh crumb
                return self.get_quote_summary_internal(symbol, &modules_str).await;
            }
        }
        
        result
    }

    /// Internal method to fetch quote summary with crumb
    async fn get_quote_summary_internal(
        &self,
        symbol: &str,
        modules_str: &[&str],
    ) -> Result<QuoteSummary> {
        let crumb = self.get_crumb().await?;
        
        let url = format!(
            "{}{}?modules={}&crumb={}",
            YAHOO_QUOTE_SUMMARY,
            symbol,
            modules_str.join(","),
            urlencoding::encode(&crumb)
        );

        let response = self.client
            .get(&url)
            .send()
            .await
            .context("Failed to send request")?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await?;
            return Err(anyhow!("HTTP {}: {}", status, error_text));
        }

        let json: Value = response.json().await?;
        
        // Parse response
        let result = &json["quoteSummary"]["result"];
        if result.is_null() || !result.is_array() || result.as_array().unwrap().is_empty() {
            return Err(anyhow!("No data for symbol: {}", symbol));
        }
        
        let result_obj = &result[0];
        let mut module_data = HashMap::new();
        
        for module_name in modules_str {
            if let Some(data) = result_obj.get(module_name) {
                if !data.is_null() {
                    module_data.insert(module_name.to_string(), data.clone());
                }
            }
        }

        Ok(QuoteSummary {
            symbol: symbol.to_string(),
            modules: module_data,
            timestamp: chrono::Utc::now().timestamp(),
        })
    }

    /// Get chart data for a symbol
    pub async fn get_chart_data(
        &self,
        symbol: &str,
        interval: &str,
        period1: i64,
        period2: i64,
    ) -> Result<ChartData> {
        self.increment_request_count().await;

        // Try request with current crumb
        let result = self.get_chart_data_internal(symbol, interval, period1, period2).await;
        
        // If unauthorized, refresh crumb and retry once
        if let Err(ref e) = result {
            let error_msg = e.to_string();
            if error_msg.contains("401") || error_msg.contains("Unauthorized") {
                self.handle_unauthorized_error(&error_msg).await?;
                return self.get_chart_data_internal(symbol, interval, period1, period2).await;
            }
        }
        
        result
    }

    /// Internal method to fetch chart data with crumb
    async fn get_chart_data_internal(
        &self,
        symbol: &str,
        interval: &str,
        period1: i64,
        period2: i64,
    ) -> Result<ChartData> {
        let crumb = self.get_crumb().await?;
        
        let url = format!(
            "{}{}?interval={}&period1={}&period2={}&crumb={}",
            YAHOO_CHART_DATA,
            symbol,
            interval,
            period1,
            period2,
            urlencoding::encode(&crumb)
        );

        let response = self.client
            .get(&url)
            .send()
            .await
            .context("Failed to send request")?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await?;
            return Err(anyhow!("HTTP {}: {}", status, error_text));
        }

        let json: Value = response.json().await?;
        
        // Parse chart data
        let result = &json["chart"]["result"];
        if result.is_null() || !result.is_array() || result.as_array().unwrap().is_empty() {
            return Err(anyhow!("No chart data for symbol: {}", symbol));
        }
        
        let chart = &result[0];
        let timestamps = chart["timestamp"].as_array().ok_or_else(|| anyhow!("No timestamp data"))?;
        let quote_data = &chart["indicators"]["quote"][0];
        
        let opens = quote_data["open"].as_array();
        let highs = quote_data["high"].as_array();
        let lows = quote_data["low"].as_array();
        let closes = quote_data["close"].as_array();
        let volumes = quote_data["volume"].as_array();
        
        let mut quotes = Vec::new();
        for (i, ts) in timestamps.iter().enumerate() {
            let timestamp = ts.as_i64().unwrap_or(0);
            
            quotes.push(Quote {
                timestamp,
                open: opens.and_then(|v| v.get(i)).and_then(|v| v.as_f64()),
                high: highs.and_then(|v| v.get(i)).and_then(|v| v.as_f64()),
                low: lows.and_then(|v| v.get(i)).and_then(|v| v.as_f64()),
                close: closes.and_then(|v| v.get(i)).and_then(|v| v.as_f64()),
                volume: volumes.and_then(|v| v.get(i)).and_then(|v| v.as_u64()),
                adjusted_close: None,
            });
        }

        Ok(ChartData {
            symbol: symbol.to_string(),
            quotes,
            timestamp: chrono::Utc::now().timestamp(),
        })
    }

    /// Get option data for a symbol
    pub async fn get_option_data(
        &self,
        symbol: &str,
        date: Option<i64>,
    ) -> Result<OptionData> {
        self.increment_request_count().await;

        // Try request with current crumb
        let result = self.get_option_data_internal(symbol, date).await;
        
        // If unauthorized, refresh crumb and retry once
        if let Err(ref e) = result {
            let error_msg = e.to_string();
            if error_msg.contains("401") || error_msg.contains("Unauthorized") {
                self.handle_unauthorized_error(&error_msg).await?;
                return self.get_option_data_internal(symbol, date).await;
            }
        }
        
        result
    }

    /// Internal method to fetch option data with crumb
    async fn get_option_data_internal(
        &self,
        symbol: &str,
        date: Option<i64>,
    ) -> Result<OptionData> {
        let crumb = self.get_crumb().await?;
        
        let url = if let Some(d) = date {
            format!(
                "{}{}?date={}&crumb={}",
                YAHOO_OPTION_DATA,
                symbol,
                d,
                urlencoding::encode(&crumb)
            )
        } else {
            format!(
                "{}{}?crumb={}",
                YAHOO_OPTION_DATA,
                symbol,
                urlencoding::encode(&crumb)
            )
        };

        let response = self.client
            .get(&url)
            .send()
            .await
            .context("Failed to send request")?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await?;
            return Err(anyhow!("HTTP {}: {}", status, error_text));
        }

        let json: Value = response.json().await?;
        
        // Parse option data
        let result = &json["optionChain"]["result"];
        if result.is_null() || !result.is_array() || result.as_array().unwrap().is_empty() {
            return Err(anyhow!("No option data for symbol: {}", symbol));
        }
        
        let option = &result[0];
        let expiration_dates = option["expirationDates"]
            .as_array()
            .map(|arr| arr.iter().filter_map(|v| v.as_i64()).collect())
            .unwrap_or_default();
        
        let strikes = option["strikes"]
            .as_array()
            .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect())
            .unwrap_or_default();
        
        let mut option_chains = Vec::new();
        
        if let Some(option_array) = option["options"].as_array() {
            for opt in option_array {
                let exp_date = opt["expirationDate"].as_i64().unwrap_or(0);
                
                let calls = Self::parse_option_contracts(&opt["calls"]);
                let puts = Self::parse_option_contracts(&opt["puts"]);
                
                option_chains.push(OptionChain {
                    expiration_date: exp_date,
                    calls,
                    puts,
                });
            }
        }

        Ok(OptionData {
            symbol: symbol.to_string(),
            expiration_dates,
            strikes,
            option: option_chains,
            timestamp: chrono::Utc::now().timestamp(),
        })
    }

    /// Parse option contracts from JSON
    fn parse_option_contracts(json: &Value) -> Vec<OptionContract> {
        json.as_array()
            .map(|arr| {
                arr.iter()
                    .filter_map(|contract| {
                        Some(OptionContract {
                            strike: contract["strike"].as_f64()?,
                            last_price: contract["lastPrice"].as_f64(),
                            bid: contract["bid"].as_f64(),
                            ask: contract["ask"].as_f64(),
                            volume: contract["volume"].as_u64(),
                            open_interest: contract["openInterest"].as_u64(),
                            implied_volatility: contract["impliedVolatility"].as_f64(),
                        })
                    })
                    .collect()
            })
            .unwrap_or_default()
    }

    /// Search for symbols
    pub async fn search_symbols(&self, query: &str) -> Result<Vec<SearchResult>> {
        self.increment_request_count().await;

        // Try request with current crumb
        let result = self.search_symbols_internal(query).await;
        
        // If unauthorized, refresh crumb and retry once
        if let Err(ref e) = result {
            let error_msg = e.to_string();
            if error_msg.contains("401") || error_msg.contains("Unauthorized") {
                self.handle_unauthorized_error(&error_msg).await?;
                return self.search_symbols_internal(query).await;
            }
        }
        
        result
    }

    /// Internal method to search symbols with crumb
    async fn search_symbols_internal(&self, query: &str) -> Result<Vec<SearchResult>> {
        let crumb = self.get_crumb().await?;
        
        let url = format!(
            "{}?q={}&crumb={}",
            YAHOO_SEARCH,
            urlencoding::encode(query),
            urlencoding::encode(&crumb)
        );

        let response = self.client
            .get(&url)
            .send()
            .await
            .context("Failed to send request")?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await?;
            return Err(anyhow!("HTTP {}: {}", status, error_text));
        }

        let json: Value = response.json().await?;
        
        // Parse search results
        let quotes = &json["quotes"];
        if quotes.is_null() || !quotes.is_array() {
            return Ok(Vec::new());
        }
        
        let results = quotes
            .as_array()
            .unwrap()
            .iter()
            .filter_map(|quote| {
                Some(SearchResult {
                    symbol: quote["symbol"].as_str()?.to_string(),
                    name: quote["shortname"].as_str().or_else(|| quote["longname"].as_str())?.to_string(),
                    exchange: quote["exchange"].as_str()?.to_string(),
                    asset_type: quote["quoteType"].as_str()?.to_string(),
                })
            })
            .collect();

        Ok(results)
    }

    /// Get client statistics
    pub async fn get_stats(&self) -> ClientStats {
        ClientStats {
            client_id: self.client_id,
            proxy_url: self.proxy_url.clone(),
            task_count: *self.task_count.lock().await,
            request_count: *self.request_count.lock().await,
            has_crumb: self.crumb.lock().await.is_some(),
            consecutive_failures: self.consecutive_failures.load(Ordering::Relaxed),
            is_marked_for_replacement: self.is_marked_for_replacement.load(Ordering::Relaxed),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ClientStats {
    pub client_id: usize,
    pub proxy_url: String,
    pub task_count: usize,
    pub request_count: u32,
    pub has_crumb: bool,
    pub consecutive_failures: usize,
    pub is_marked_for_replacement: bool,
}

/// YahooClientPool - Manages pool of YahooClients with rotation and auto-replacement
pub struct YahooClientPool {
    proxy_pool: Arc<DockerVpnProxyPool>,
    clients: Arc<RwLock<Vec<Arc<YahooClient>>>>,
    next_client: Arc<Mutex<usize>>,
    next_proxy_index: Arc<Mutex<usize>>,  // For round-robin proxy assignment
    semaphore: Arc<Semaphore>,
    rotation_enabled: bool,
    max_tasks_per_client: usize,
    monitoring: Option<crate::monitoring::MonitoringHandle>,
}

impl YahooClientPool {
    /// Create new YahooClientPool with mandatory proxy pool
    pub async fn new(
        proxy_pool: Arc<DockerVpnProxyPool>,
        config: &Config,
        monitoring: Option<crate::monitoring::MonitoringHandle>,
    ) -> Result<Self> {
        let pool_size_limit = config.max_parallel_instances;
        let max_tasks_per_instance = config.max_tasks_per_instance;
        
        // Pool size is limited by available proxies
        let actual_pool_size = pool_size_limit.min(proxy_pool.num_proxies());
        
        if actual_pool_size == 0 {
            return Err(anyhow!("Pool size must be at least 1"));
        }
        
        // Rotation: only half of pool is active at once
        let rotation_enabled = max_tasks_per_instance > 0;
        let half_size = if rotation_enabled {
            (actual_pool_size + 1) / 2
        } else {
            actual_pool_size
        };
        
        logger::log_info(&format!(
            "Initializing YahooClientPool with {} clients (each using a unique SOCKS5 proxy) with rotation {}...",
            actual_pool_size,
            if rotation_enabled { "enabled" } else { "disabled" }
        )).await;
                
        logger::log_info(&format!(
            "Initializing {} YahooClients in parallel...",
            actual_pool_size
        )).await;

        let mut handles = Vec::new();

        for i in 0..actual_pool_size {
            let proxy_url = proxy_pool.get_proxy_url(i);
            let monitoring_clone = monitoring.clone();
            
            let handle = tokio::spawn(async move {
                YahooClient::new(i, proxy_url, max_tasks_per_instance, monitoring_clone).await
            });
            
            handles.push(handle);
        }

        // Wait for all to complete
        let results = futures::future::join_all(handles).await;

        let mut clients = Vec::with_capacity(actual_pool_size);
        for (i, result) in results.into_iter().enumerate() {
            match result {
                Ok(Ok(client)) => {
                    clients.push(Arc::new(client));
                    logger::log_info(&format!("  ✓ Client {} ready", i)).await;
                }
                Ok(Err(e)) => {
                    logger::log_warn(&format!(
                        "  ✗ Failed to initialize Client {}: {} - skipping",
                        i, e
                    )).await;
                }
                Err(e) => {
                    logger::log_warn(&format!(
                        "  ✗ Client {} task panicked: {} - skipping",
                        i, e
                    )).await;
                }
            }
        }

        if clients.is_empty() {
            return Err(anyhow!(
                "Failed to initialize any YahooClients - all {} proxy connections failed",
                actual_pool_size
            ));
        }

        if clients.len() < actual_pool_size {
            logger::log_warn(&format!(
                "⚠ Pool initialized with {}/{} clients (some connections failed)",
                clients.len(), actual_pool_size
            )).await;
        } else {
            logger::log_info(&format!(
                "✓ YahooClientPool initialized with {} clients (active: {})",
                clients.len(),
                if rotation_enabled { (clients.len() + 1) / 2 } else { clients.len() }
            )).await;
        }
        
        Ok(Self {
            proxy_pool,
            clients: Arc::new(RwLock::new(clients)),
            next_client: Arc::new(Mutex::new(0)),
            next_proxy_index: Arc::new(Mutex::new(actual_pool_size)),  // Start after initial clients
            semaphore: Arc::new(Semaphore::new(half_size)),
            rotation_enabled,
            max_tasks_per_client: max_tasks_per_instance,
            monitoring,
        })
    }

    /// Replace a marked client with a new one using next available proxy (round-robin)
    async fn replace_marked_client(&self, index: usize) -> Result<()> {
        let clients_write = self.clients.write().await;
        
        if index >= clients_write.len() {
            return Err(anyhow!("Invalid client index: {}", index));
        }
        
        let old_client = &clients_write[index];
        let old_client_id = old_client.get_client_id();
        let old_proxy_url = old_client.get_proxy_url().to_string();
        
        logger::log_info(&format!(
            "🔄 Replacing YahooClient[{}] (proxy: {})...",
            old_client_id, old_proxy_url
        )).await;
        
        drop(clients_write);  // Release write lock during retry attempts
        
        // NEW: Try multiple proxies with health checking
        const MAX_RETRY_ATTEMPTS: usize = 5;
        
        let mut next_proxy = self.next_proxy_index.lock().await;
        let start_proxy_index = *next_proxy;
        
        for attempt in 0..MAX_RETRY_ATTEMPTS {
            let current_proxy_index = (start_proxy_index + attempt) % self.proxy_pool.num_proxies();
            
            // NEW: Get healthy proxy URL (skips dead proxies)
            let (proxy_index, new_proxy_url) = match self.proxy_pool.get_healthy_proxy_url(current_proxy_index).await {
                Some(proxy_info) => proxy_info,
                None => {
                    logger::log_error("❌ No healthy proxies available!").await;
                    drop(next_proxy);
                    return Err(anyhow!("All proxies are dead"));
                }
            };
            
            logger::log_info(&format!(
                "  Attempt {}/{}: Trying proxy {} ({})",
                attempt + 1, MAX_RETRY_ATTEMPTS, proxy_index, new_proxy_url
            )).await;
            
            // Try to create new client
            match YahooClient::new(
                old_client_id,
                new_proxy_url.clone(),
                self.max_tasks_per_client,
                self.monitoring.clone(),
            ).await {
                Ok(new_client) => {
                    // SUCCESS: Replace the client
                    let mut clients_write = self.clients.write().await;
                    clients_write[index] = Arc::new(new_client);
                    
                    // Update next_proxy_index for next replacement
                    *next_proxy = proxy_index + 1;
                    drop(next_proxy);
                    
                    logger::log_info(&format!(
                        "✓ Replaced YahooClient[{}] with proxy: {}",
                        old_client_id, new_proxy_url
                    )).await;
                    
                    return Ok(());
                }
                Err(e) => {
                    logger::log_warn(&format!(
                        "  ⚠ Proxy {} failed: {}",
                        proxy_index, e
                    )).await;
                    
                    // NEW: Mark this proxy as dead
                    self.proxy_pool.mark_proxy_dead(proxy_index).await;
                    
                    // Continue to next proxy
                    continue;
                }
            }
        }
        
        drop(next_proxy);
        
        // FAILED after all attempts
        logger::log_error(&format!(
            "❌ Failed to replace YahooClient[{}] after {} attempts",
            old_client_id, MAX_RETRY_ATTEMPTS
        )).await;
        
        // NEW: Instead of removing client, keep it marked for replacement
        // Background task will retry later
        Err(anyhow!("Failed to replace client after {} attempts", MAX_RETRY_ATTEMPTS))
    }

    /// Execute a task using an available client with round-robin selection
    /// Automatically handles client replacement when failures occur
    pub async fn execute<T, F, Fut>(&self, f: F) -> Result<T>
    where
        F: FnOnce(Arc<YahooClient>) -> Fut + Send + 'static,
        Fut: std::future::Future<Output = Result<T>> + Send,
        T: Send + 'static,
    {
        // Acquire semaphore permit (limits concurrency)
        let _permit = self.semaphore.acquire().await?;
        
        // NEW: Add timeout to client selection
        const CLIENT_SELECTION_TIMEOUT_SECS: u64 = 30;
        
        let selection_future = async {
            // Select client with round-robin + availability check
            Ok('select: loop {
                let clients_read = self.clients.read().await;
                let mut next_idx = self.next_client.lock().await;
                let start_idx = *next_idx;
                let total = clients_read.len();
                let mut checked = 0;

                // Try to find an available client
                while checked < total {
                    let idx = *next_idx;
                    let client = &clients_read[idx];

                    // Move to next for next iteration
                    *next_idx = (*next_idx + 1) % total;
                    checked += 1;

                    // Skip marked clients
                    if !client.is_marked_for_replacement() && client.is_available().await {
                        break 'select (Arc::clone(client), idx);
                    }
                }

                // NEW: Check if all clients are unavailable
                let all_marked = clients_read.iter().all(|c| c.is_marked_for_replacement());
                
                drop(next_idx);
                drop(clients_read);
                
                if all_marked {
                    // All clients marked - this is a critical error
                    return Err(anyhow!(
                        "All {} clients are marked for replacement - pool critically degraded",
                        total
                    ));
                }
                
                sleep(Duration::from_millis(100)).await;
            })
        };
        
        // NEW: Wrap in timeout to prevent infinite loop
        let (client, client_index) = match timeout(
            Duration::from_secs(CLIENT_SELECTION_TIMEOUT_SECS),
            selection_future
        ).await {
            Ok(Ok(result)) => result,
            Ok(Err(e)) => return Err(e),
            Err(_) => {
                return Err(anyhow!(
                    "Timeout waiting for available client (>{}s) - pool is overloaded or degraded",
                    CLIENT_SELECTION_TIMEOUT_SECS
                ));
            }
        };
        
        // Increment task count
        client.increment_task_count().await;
        
        // Execute task
        let result = f(Arc::clone(&client)).await;
        
        // Handle result and track failures
        match &result {
            Ok(_) => {
                client.record_success();
            }
            Err(e) => {
                let error_str = e.to_string();
                let should_replace = client.record_failure(&error_str).await;
                
                // Trigger replacement if needed (non-blocking)
                if should_replace {
                    let pool_clone = Arc::new(self.clone());
                    tokio::spawn(async move {
                        if let Err(e) = pool_clone.replace_marked_client(client_index).await {
                            logger::log_error(&format!(
                                "Failed to replace client in background: {}",
                                e
                            )).await;
                        }
                    });
                }
            }
        }
        
        // Decrement task count
        client.decrement_task_count().await;
        
        result
    }

    /// Check for and replace any marked clients (can be called periodically)
    pub async fn check_and_replace_marked_clients(&self) -> Result<usize> {
        let clients_read = self.clients.read().await;
        let mut marked_indices = Vec::new();
        
        for (idx, client) in clients_read.iter().enumerate() {
            if client.is_marked_for_replacement() {
                marked_indices.push(idx);
            }
        }
        drop(clients_read);
        
        if marked_indices.is_empty() {
            return Ok(0);
        }
        
        logger::log_info(&format!(
            "Found {} marked clients, replacing...",
            marked_indices.len()
        )).await;
        
        let mut replaced_count = 0;
        for idx in marked_indices {
            if self.replace_marked_client(idx).await.is_ok() {
                replaced_count += 1;
            }
        }
        
        Ok(replaced_count)
    }

    /// Get quote summary for a symbol
    pub async fn get_quote_summary(
        &self,
        symbol: &str,
        modules: &[QuoteSummaryModule],
    ) -> Result<QuoteSummary> {
        let symbol = symbol.to_string();
        let modules = modules.to_vec();
        
        self.execute(move |client| async move {
            client.get_quote_summary(&symbol, &modules).await
        }).await
    }

    /// Get chart data for a symbol
    pub async fn get_chart_data(
        &self,
        symbol: &str,
        interval: &str,
        period1: i64,
        period2: i64,
    ) -> Result<ChartData> {
        let symbol = symbol.to_string();
        let interval = interval.to_string();
        
        self.execute(move |client| async move {
            client.get_chart_data(&symbol, &interval, period1, period2).await
        }).await
    }

    /// Get option data for a symbol
    pub async fn get_option_data(
        &self,
        symbol: &str,
        date: Option<i64>,
    ) -> Result<OptionData> {
        let symbol = symbol.to_string();
        
        self.execute(move |client| async move {
            client.get_option_data(&symbol, date).await
        }).await
    }

    /// Search for symbols
    pub async fn search_symbols(&self, query: &str) -> Result<Vec<SearchResult>> {
        let query = query.to_string();
        
        self.execute(move |client| async move {
            client.search_symbols(&query).await
        }).await
    }

    /// Get number of clients in pool
    pub async fn num_clients(&self) -> usize {
        self.clients.read().await.len()
    }

    /// Check if rotation is enabled
    pub fn is_rotation_enabled(&self) -> bool {
        self.rotation_enabled
    }

    /// Get pool statistics
    pub async fn get_pool_stats(&self) -> PoolStats {
        let clients_read = self.clients.read().await;
        let mut client_stats = Vec::new();
        let mut marked_count = 0;
        
        for client in clients_read.iter() {
            let stats = client.get_stats().await;
            if stats.is_marked_for_replacement {
                marked_count += 1;
            }
            client_stats.push(stats);
        }
        
        PoolStats {
            total_clients: clients_read.len(),
            rotation_enabled: self.rotation_enabled,
            marked_for_replacement: marked_count,
            clients: client_stats,
        }
    }

    /// Shutdown the pool (graceful cleanup)
    pub async fn shutdown(&self) -> Result<()> {
        logger::log_info("Shutting down YahooClientPool...").await;
        
        // Clients will be dropped and cleaned up automatically
        
        logger::log_info("✓ YahooClientPool shutdown complete").await;
        Ok(())
    }
}

// Implement Clone for YahooClientPool to allow spawning background tasks
impl Clone for YahooClientPool {
    fn clone(&self) -> Self {
        Self {
            proxy_pool: Arc::clone(&self.proxy_pool),
            clients: Arc::clone(&self.clients),
            next_client: Arc::clone(&self.next_client),
            next_proxy_index: Arc::clone(&self.next_proxy_index),
            semaphore: Arc::clone(&self.semaphore),
            rotation_enabled: self.rotation_enabled,
            max_tasks_per_client: self.max_tasks_per_client,
            monitoring: self.monitoring.clone(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct PoolStats {
    pub total_clients: usize,
    pub rotation_enabled: bool,
    pub marked_for_replacement: usize,
    pub clients: Vec<ClientStats>,
}