//! Parallel aggTrades fetching for sub-3s gap recovery (Issue #257).
//!
//! Fetches multiple pages of aggTrades concurrently using `tokio::JoinSet`
//! with a semaphore to bound concurrency. Pages are reassembled in
//! `agg_trade_id` order before returning — the processor is strictly
//! order-sensitive and relies on monotonic trade IDs.
//!
//! # Performance
//!
//! Sequential: 50K trades = ~50 REST calls × 500ms ≈ 25s
//! Parallel (5 concurrent): 50K trades = ~10 batches × 500ms ≈ 5s
//! Target: sub-3s for typical reconnection gaps (~5-10K trades)

use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
use tracing::{debug, info};

use opendeviationbar_core::AggTrade;

use super::historical::HistoricalDataLoader;
use super::rate_limiter::AdaptiveRateLimiter;

/// Default concurrency for parallel fetch (number of simultaneous REST calls).
const DEFAULT_CONCURRENCY: usize = 5;

/// Binance aggTrades page size (maximum per REST call).
const PAGE_SIZE: i64 = 1000;

/// Fetch aggTrades in parallel across multiple pages.
///
/// Pages are fetched concurrently (up to `concurrency` at a time) and
/// reassembled in ascending `agg_trade_id` order before returning.
///
/// # Arguments
///
/// * `symbol` - Trading pair (e.g., "BTCUSDT")
/// * `from_id` - First trade ID to fetch (inclusive)
/// * `to_id` - Upper bound trade ID (exclusive) — trades with id >= to_id are excluded
/// * `rate_limiter` - Shared rate limiter for Binance REST API budget
/// * `concurrency` - Maximum concurrent REST calls (None = DEFAULT_CONCURRENCY)
///
/// # Returns
///
/// Sorted `Vec<AggTrade>` with contiguous trade IDs in `[from_id, to_id)`.
///
/// # Errors
///
/// Returns error if any page fetch fails. Partial results are NOT returned —
/// callers should handle the error and potentially retry or defer to kintsugi.
pub async fn fetch_aggtrades_parallel(
    symbol: &str,
    from_id: i64,
    to_id: i64,
    rate_limiter: Option<Arc<AdaptiveRateLimiter>>,
    concurrency: Option<usize>,
) -> Result<Vec<AggTrade>, super::historical::HistoricalError> {
    let concurrency = concurrency.unwrap_or(DEFAULT_CONCURRENCY);
    let total_trades = to_id - from_id;

    if total_trades <= 0 {
        return Ok(Vec::new());
    }

    // For small gaps, just fetch sequentially (overhead of JoinSet not worth it)
    if total_trades <= PAGE_SIZE {
        let loader = match &rate_limiter {
            Some(rl) => HistoricalDataLoader::new(symbol).with_rate_limiter(Arc::clone(rl)),
            None => HistoricalDataLoader::new(symbol),
        };
        let limit = total_trades.min(1000) as u16;
        let mut trades = loader.fetch_aggtrades_by_id(from_id, limit).await?;
        // Trim to requested range
        trades.retain(|t| t.agg_trade_id >= from_id && t.agg_trade_id < to_id);
        return Ok(trades);
    }

    // Calculate page boundaries
    let mut page_starts: Vec<i64> = Vec::new();
    let mut start = from_id;
    while start < to_id {
        page_starts.push(start);
        start += PAGE_SIZE;
    }

    let num_pages = page_starts.len();
    info!(
        symbol,
        from_id,
        to_id,
        total_trades,
        num_pages,
        concurrency,
        "starting parallel aggTrades fetch"
    );

    let semaphore = Arc::new(Semaphore::new(concurrency));
    let mut join_set = JoinSet::new();

    for (page_idx, page_start) in page_starts.into_iter().enumerate() {
        let permit = Arc::clone(&semaphore);
        let rate_limiter = rate_limiter.clone();
        let symbol = symbol.to_string();
        let limit = ((to_id - page_start).min(PAGE_SIZE)) as u16;

        join_set.spawn(async move {
            let _permit = permit.acquire().await.expect("semaphore closed");

            // Note: rate limiting is handled inside fetch_aggtrades_by_id via
            // send_with_rate_limit_backoff(), so we do NOT acquire separately here.
            let loader = match &rate_limiter {
                Some(rl) => HistoricalDataLoader::new(&symbol).with_rate_limiter(Arc::clone(rl)),
                None => HistoricalDataLoader::new(&symbol),
            };

            let trades = loader.fetch_aggtrades_by_id(page_start, limit).await?;

            debug!(
                symbol = %symbol,
                page_idx,
                page_start,
                trades_fetched = trades.len(),
                "parallel fetch page complete"
            );

            Ok::<(usize, Vec<AggTrade>), super::historical::HistoricalError>((page_idx, trades))
        });
    }

    // Collect all pages
    let mut pages: Vec<(usize, Vec<AggTrade>)> = Vec::with_capacity(num_pages);
    while let Some(result) = join_set.join_next().await {
        match result {
            Ok(Ok(page)) => pages.push(page),
            Ok(Err(e)) => {
                // Abort remaining tasks on any page failure
                join_set.abort_all();
                return Err(e);
            }
            Err(e) => {
                join_set.abort_all();
                return Err(super::historical::HistoricalError::RestApiError(format!(
                    "parallel fetch task panicked: {e}"
                )));
            }
        }
    }

    // CRITICAL (P3): Reassemble in page order (ascending agg_trade_id)
    pages.sort_by_key(|(idx, _)| *idx);

    // Flatten and trim to requested range
    let mut all_trades: Vec<AggTrade> = Vec::with_capacity(total_trades as usize);
    for (_, mut page_trades) in pages {
        page_trades.retain(|t| t.agg_trade_id >= from_id && t.agg_trade_id < to_id);
        all_trades.extend(page_trades);
    }

    // Dedup by agg_trade_id (handles overlapping page boundaries)
    all_trades.sort_by_key(|t| t.agg_trade_id);
    all_trades.dedup_by_key(|t| t.agg_trade_id);

    info!(
        symbol,
        from_id,
        to_id,
        trades_fetched = all_trades.len(),
        num_pages,
        "parallel aggTrades fetch complete"
    );

    Ok(all_trades)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_page_boundary_calculation() {
        // 2500 trades = 3 pages: [0, 1000), [1000, 2000), [2000, 2500)
        let from_id = 100;
        let to_id = 2600;
        let total = to_id - from_id;

        let mut starts = Vec::new();
        let mut start = from_id;
        while start < to_id {
            starts.push(start);
            start += PAGE_SIZE;
        }

        assert_eq!(starts.len(), 3);
        assert_eq!(starts[0], 100);
        assert_eq!(starts[1], 1100);
        assert_eq!(starts[2], 2100);
        assert_eq!(total, 2500);
    }

    #[test]
    fn test_small_gap_no_parallel() {
        // Gaps <= 1000 trades should NOT use JoinSet
        let total = PAGE_SIZE;
        assert!(total <= PAGE_SIZE); // Should use single fetch path
    }

    #[tokio::test]
    async fn test_empty_range() {
        let result = fetch_aggtrades_parallel("BTCUSDT", 100, 100, None, None).await;
        assert!(result.is_ok());
        assert!(result.unwrap().is_empty());
    }

    #[tokio::test]
    async fn test_negative_range() {
        let result = fetch_aggtrades_parallel("BTCUSDT", 200, 100, None, None).await;
        assert!(result.is_ok());
        assert!(result.unwrap().is_empty());
    }
}
