use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Instant;

use pyo3::prelude::*;
use tokio::fs;
use tokio::sync::{mpsc, Semaphore};
use tokio::time::{timeout, sleep, Duration};

use crate::{analysis::get_file_extension, AnalysisConfig, ParallelDirectoryStats};

// Async scanning implementation tuned for higher-latency filesystems (NFS/CIFS)
// Uses a work-queue pattern with multiple workers for true parallel directory scanning.

// Backoff constants for retry logic
const READDIR_BACKOFF_BASE_MS: u64 = 50;
const METADATA_BACKOFF_BASE_MS: u64 = 10;

// Work queue channel capacity
const WORK_QUEUE_CAPACITY: usize = 10000;

/// Tracks I/O errors and timeouts during async scanning (for diagnostics)
#[derive(Debug, Default)]
struct ScanMetrics {
    readdir_timeouts: AtomicU64,
    readdir_errors: AtomicU64,
    metadata_timeouts: AtomicU64,
    metadata_errors: AtomicU64,
    skipped_entries: AtomicU64,
}

impl ScanMetrics {
    fn new() -> Self {
        Self::default()
    }

    fn record_readdir_timeout(&self) {
        self.readdir_timeouts.fetch_add(1, Ordering::Relaxed);
    }

    fn record_readdir_error(&self) {
        self.readdir_errors.fetch_add(1, Ordering::Relaxed);
    }

    fn record_metadata_timeout(&self) {
        self.metadata_timeouts.fetch_add(1, Ordering::Relaxed);
    }

    fn record_metadata_error(&self) {
        self.metadata_errors.fetch_add(1, Ordering::Relaxed);
    }

    fn record_skipped_entry(&self) {
        self.skipped_entries.fetch_add(1, Ordering::Relaxed);
    }

    /// Returns true if any errors/timeouts were recorded
    fn has_issues(&self) -> bool {
        self.readdir_timeouts.load(Ordering::Relaxed) > 0
            || self.readdir_errors.load(Ordering::Relaxed) > 0
            || self.metadata_timeouts.load(Ordering::Relaxed) > 0
            || self.metadata_errors.load(Ordering::Relaxed) > 0
    }

    /// Log a summary if there were any issues (useful for debugging)
    #[allow(dead_code)]
    fn summary(&self) -> String {
        format!(
            "ScanMetrics {{ readdir_timeouts: {}, readdir_errors: {}, metadata_timeouts: {}, metadata_errors: {}, skipped: {} }}",
            self.readdir_timeouts.load(Ordering::Relaxed),
            self.readdir_errors.load(Ordering::Relaxed),
            self.metadata_timeouts.load(Ordering::Relaxed),
            self.metadata_errors.load(Ordering::Relaxed),
            self.skipped_entries.load(Ordering::Relaxed),
        )
    }
}

/// Work item representing a directory to scan
struct WorkItem {
    path: PathBuf,
    depth: u32,
}

pub async fn probe_directory_async_internal(
    path_root: PathBuf,
    config: AnalysisConfig,
    concurrency_limit: usize,
    timeout_ms: u64,
    retries: u8,
) -> Result<crate::DirectoryStats, String> {
    let start = Instant::now();

    // Use the ParallelDirectoryStats structure for thread-safe aggregation
    let stats = Arc::new(ParallelDirectoryStats::new());
    // Semaphore limits concurrent I/O operations (read_dir calls)
    let sem = Arc::new(Semaphore::new(concurrency_limit));
    let metrics = Arc::new(ScanMetrics::new());

    // Count the root directory only if it is non-empty
    if let Some(name) = path_root.file_name().and_then(|n| n.to_str()) {
        let is_empty = crate::analysis::estimate_directory_size(&path_root, 1) == 0;
        if !is_empty {
            stats.add_folder(name.to_string(), false, path_root.to_string_lossy().to_string(), 0);
        }
    }

    // Create work queue channel for distributing directory scanning work
    let (tx, rx) = mpsc::channel::<WorkItem>(WORK_QUEUE_CAPACITY);
    let rx = Arc::new(tokio::sync::Mutex::new(rx));

    // Track pending work to know when we're done
    let pending_work = Arc::new(AtomicUsize::new(1)); // Start with 1 for root

    // Send root directory to work queue
    tx.send(WorkItem { path: path_root.clone(), depth: 0 })
        .await
        .map_err(|e| format!("Failed to send root to work queue: {}", e))?;

    // Spawn worker tasks - these will pull work from the queue
    // Use concurrency_limit as the number of workers since that's user-configurable
    let num_workers = concurrency_limit.max(4);
    let mut worker_handles = Vec::with_capacity(num_workers);

    for _ in 0..num_workers {
        let worker_rx = rx.clone();
        let worker_tx = tx.clone();
        let worker_stats = stats.clone();
        let worker_config = config.clone();
        let worker_sem = sem.clone();
        let worker_metrics = metrics.clone();
        let worker_pending = pending_work.clone();

        let handle = tokio::spawn(async move {
            loop {
                // Try to get work from the queue
                let work_item = {
                    let mut rx_guard = worker_rx.lock().await;
                    // Use try_recv to avoid blocking indefinitely
                    match tokio::time::timeout(Duration::from_millis(10), rx_guard.recv()).await {
                        Ok(Some(item)) => item,
                        Ok(None) => break, // Channel closed
                        Err(_) => {
                            // Timeout - check if we should exit
                            if worker_pending.load(Ordering::SeqCst) == 0 {
                                break;
                            }
                            continue;
                        }
                    }
                };

                // Process this directory
                let subdirs = process_directory(
                    &work_item.path,
                    work_item.depth,
                    &worker_stats,
                    &worker_config,
                    &worker_sem,
                    &worker_metrics,
                    timeout_ms,
                    retries,
                ).await;

                // Add subdirectories to work queue
                let subdir_count = subdirs.len();
                if subdir_count > 0 {
                    worker_pending.fetch_add(subdir_count, Ordering::SeqCst);
                    for subdir in subdirs {
                        let _ = worker_tx.send(WorkItem {
                            path: subdir,
                            depth: work_item.depth + 1,
                        }).await;
                    }
                }

                // Mark this work item as complete
                worker_pending.fetch_sub(1, Ordering::SeqCst);
            }
        });

        worker_handles.push(handle);
    }

    // Drop the sender in the main task so workers can detect completion
    drop(tx);

    // Wait for all workers to finish
    for handle in worker_handles {
        let _ = handle.await;
    }

    // Convert to DirectoryStats and set timing
    let mut result = stats.to_directory_stats();
    let elapsed = start.elapsed();
    result.set_timing(elapsed.as_secs_f64());

    // Log metrics summary if there were issues
    if metrics.has_issues() {
        #[cfg(debug_assertions)]
        eprintln!("[filoma async] {}", metrics.summary());
    }

    Ok(result)
}

/// Process a single directory and return list of subdirectories to scan
async fn process_directory(
    dir: &PathBuf,
    current_depth: u32,
    stats: &Arc<ParallelDirectoryStats>,
    config: &AnalysisConfig,
    sem: &Arc<Semaphore>,
    metrics: &Arc<ScanMetrics>,
    timeout_ms: u64,
    retries: u8,
) -> Vec<PathBuf> {
    // Respect max_depth
    if let Some(max_d) = config.max_depth {
        if current_depth > max_d {
            return Vec::new();
        }
    }

    // Acquire a permit to limit concurrent I/O operations
    let permit = match sem.acquire().await {
        Ok(p) => p,
        Err(_) => return Vec::new(),
    };

    // Read directory entries with timeout & retries
    let mut attempt = 0u8;
    let read_dir = loop {
        let fut = fs::read_dir(dir);
        match timeout(Duration::from_millis(timeout_ms), fut).await {
            Ok(Ok(rd)) => break rd,
            Ok(Err(_e)) => {
                metrics.record_readdir_error();
            }
            Err(_) => {
                metrics.record_readdir_timeout();
            }
        }

        if attempt >= retries {
            metrics.record_skipped_entry();
            drop(permit);
            return Vec::new();
        }
        attempt += 1;
        sleep(Duration::from_millis(READDIR_BACKOFF_BASE_MS * (1 << attempt.min(4)))).await;
    };

    // Release permit after read_dir succeeds
    drop(permit);

    // Collect entries and subdirectories
    let mut entries = read_dir;
    let mut is_empty = true;
    let mut subdirs: Vec<PathBuf> = Vec::new();

    while let Ok(Some(entry)) = entries.next_entry().await {
        is_empty = false;
        let path = entry.path();

        // Metadata with timeout & retries
        let mut meta_attempt = 0u8;
        let metadata = loop {
            match timeout(Duration::from_millis(timeout_ms), entry.metadata()).await {
                Ok(Ok(md)) => break Some(md),
                Ok(Err(_e)) => {
                    metrics.record_metadata_error();
                }
                Err(_) => {
                    metrics.record_metadata_timeout();
                }
            }

            if meta_attempt >= retries {
                break None;
            }
            meta_attempt += 1;
            sleep(Duration::from_millis(METADATA_BACKOFF_BASE_MS * (1 << meta_attempt.min(4)))).await;
        };

        if let Some(md) = metadata {
            if md.is_dir() {
                // Record the folder
                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                    stats.add_folder(
                        name.to_string(),
                        false,
                        path.to_string_lossy().to_string(),
                        current_depth,
                    );
                }
                // Queue for parallel processing
                subdirs.push(path);
            } else if md.is_file() {
                let ext = get_file_extension(&path);
                let size = if config.fast_path_only { 0 } else { md.len() };
                let parent = path.parent().map(|p| p.to_string_lossy().to_string()).unwrap_or_default();
                stats.add_file(size, ext, parent, config.fast_path_only);
            }
        } else {
            metrics.record_skipped_entry();
        }
    }

    // Record empty directory
    if is_empty {
        if let Some(name) = dir.file_name().and_then(|n| n.to_str()) {
            stats.add_folder(name.to_string(), true, dir.to_string_lossy().to_string(), current_depth);
        }
    }

    subdirs
}

#[pyfunction]
#[pyo3(signature = (path_root, max_depth=None, concurrency_limit=None, timeout_ms=None, retries=None, fast_path_only=None, follow_links=None, search_hidden=None, no_ignore=None))]
pub(crate) fn probe_directory_rust_async(path_root: &str, max_depth: Option<u32>, concurrency_limit: Option<usize>, timeout_ms: Option<u64>, retries: Option<u8>, fast_path_only: Option<bool>, follow_links: Option<bool>, search_hidden: Option<bool>, no_ignore: Option<bool>) -> PyResult<PyObject> {
    let root = PathBuf::from(path_root);

    if !root.exists() {
        return Err(pyo3::exceptions::PyValueError::new_err(format!("Path does not exist: {}", path_root)));
    }
    if !root.is_dir() {
        return Err(pyo3::exceptions::PyValueError::new_err(format!("Path is not a directory: {}", path_root)));
    }

    // Build config
    let config = AnalysisConfig {
        max_depth,
        follow_links: follow_links.unwrap_or(true),
        search_hidden: search_hidden.unwrap_or(true),
        no_ignore: no_ignore.unwrap_or(true),
        parallel: true,
        parallel_threshold: 1000,
        log_progress: false,
        fast_path_only: fast_path_only.unwrap_or(false),
    };

    let concurrency = concurrency_limit.unwrap_or(64);
    let op_timeout_ms = timeout_ms.unwrap_or(5000);
    let retries = retries.unwrap_or(0);

    // Build a runtime and block_on the async analysis
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to build tokio runtime: {}", e)))?;

    // Wrap the internal call to inject timeout/retry behavior into a config closure
    let stats = rt.block_on(async move {
        probe_directory_async_internal(root, config, concurrency, op_timeout_ms, retries).await
    });

    let stats = stats.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e))?;

    Python::with_gil(|py| stats.to_py_dict(py, path_root))
}
