//! Copy engine orchestrator: the main pipeline that ties listing, filtering,
//! concurrency, copying, and progress reporting together.

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

use tokio::sync::{Semaphore, mpsc};
use tracing::{debug, info};

use crate::config::CopyConfig;
use crate::engine::checkpoint::Checkpoint;
use crate::engine::concurrency::AdaptiveConcurrency;
use crate::error::Result;
use crate::filter::FilterChain;
use crate::progress::reporter::ProgressState;
use crate::s3::client::DualClient;
use crate::s3::ops::S3Operations;
use crate::s3::real::RealS3;
use crate::types::{
    CopyManifest, CopyOutcome, CopyResult, CopyStrategy, ObjectInfo, S3Uri, SkipReason,
};

/// Maximum retries per copy job.
const MAX_RETRIES: u32 = 3;
/// Initial backoff duration.
const INITIAL_BACKOFF: Duration = Duration::from_secs(1);
/// Maximum backoff duration.
const MAX_BACKOFF: Duration = Duration::from_secs(30);

/// Run the full copy pipeline using real AWS S3 clients.
///
/// This is the main entry point for production use (CLI, Python bindings).
pub async fn run(config: CopyConfig, progress: Arc<ProgressState>) -> Result<CopyManifest> {
    let clients = DualClient::new(
        config.source_profile.as_deref(),
        config.dest_profile.as_deref(),
        None,
        None,
    )
    .await?;

    let s3: Arc<dyn S3Operations> = Arc::new(RealS3 {
        source: clients.source,
        dest: clients.destination,
    });

    run_with_s3(config, progress, s3).await
}

/// Run the full copy pipeline with an injected S3 implementation.
///
/// Use this for testing with `MockS3` or any custom `S3Operations` impl.
pub async fn run_with_s3(
    config: CopyConfig,
    progress: Arc<ProgressState>,
    s3: Arc<dyn S3Operations>,
) -> Result<CopyManifest> {
    let start = Instant::now();

    // Build filter chain.
    let filter = Arc::new(FilterChain::from_config(&config.filters)?);

    // Build checkpoint.
    let checkpoint = Arc::new(match &config.checkpoint_path {
        Some(path) => Checkpoint::new(path, config.resume)?,
        None => Checkpoint::noop(),
    });

    // Concurrency control.
    let initial_permits = config.concurrency.max_concurrent as usize;
    let concurrency_ctrl = Arc::new(AdaptiveConcurrency::new(
        config.concurrency.max_concurrent,
        config.concurrency.min_concurrent,
        config.concurrency.max_concurrent * 4,
        config.concurrency.adaptive,
    ));

    // Dynamic semaphore: start with initial_permits, resize based on AIMD.
    let semaphore = Arc::new(Semaphore::new(initial_permits));
    let active_permits = Arc::new(AtomicUsize::new(initial_permits));

    // Channels.
    let (list_tx, mut list_rx) =
        mpsc::channel::<ObjectInfo>(config.concurrency.listing_buffer_size);
    let (result_tx, mut result_rx) = mpsc::channel::<CopyResult>(1024);

    // Counters for the manifest.
    let copied_objects = Arc::new(AtomicU64::new(0));
    let copied_bytes = Arc::new(AtomicU64::new(0));
    let skipped_objects = Arc::new(AtomicU64::new(0));
    let failed_objects = Arc::new(AtomicU64::new(0));
    let failed_keys: Arc<tokio::sync::Mutex<Vec<String>>> =
        Arc::new(tokio::sync::Mutex::new(Vec::new()));

    // Spawn listing task.
    let list_s3 = s3.clone();
    let list_source = config.source.clone();
    let list_handle = tokio::spawn(async move {
        let objects = list_s3.list_objects(&list_source).await?;
        let total = objects.len() as u64;
        for info in objects {
            if list_tx.send(info).await.is_err() {
                break; // receiver dropped
            }
        }
        Ok::<u64, crate::error::S3boltError>(total)
    });

    // Spawn result collector.
    let collector_copied = copied_objects.clone();
    let collector_bytes = copied_bytes.clone();
    let collector_skipped = skipped_objects.clone();
    let collector_failed = failed_objects.clone();
    let collector_failed_keys = failed_keys.clone();
    let collector_checkpoint = checkpoint.clone();
    let collector_progress = progress.clone();
    let collector_handle = tokio::spawn(async move {
        while let Some(result) = result_rx.recv().await {
            match &result.outcome {
                CopyOutcome::Success { .. } => {
                    collector_copied.fetch_add(1, Ordering::Relaxed);
                    collector_bytes.fetch_add(result.bytes_copied, Ordering::Relaxed);
                    collector_progress
                        .objects_completed
                        .fetch_add(1, Ordering::Relaxed);
                    collector_progress
                        .bytes_completed
                        .fetch_add(result.bytes_copied, Ordering::Relaxed);
                    let _ = collector_checkpoint.mark_completed(&result.source_key);
                }
                CopyOutcome::Skipped { .. } => {
                    collector_skipped.fetch_add(1, Ordering::Relaxed);
                    collector_progress
                        .objects_completed
                        .fetch_add(1, Ordering::Relaxed);
                }
                CopyOutcome::Failed { .. } => {
                    collector_failed.fetch_add(1, Ordering::Relaxed);
                    collector_progress
                        .objects_failed
                        .fetch_add(1, Ordering::Relaxed);
                    collector_failed_keys
                        .lock()
                        .await
                        .push(result.source_key.clone());
                }
            }
        }
    });

    // Dispatch copy tasks from listing results.
    let source_prefix = config.source.clone();
    let dest = config.destination.clone();
    let dry_run = config.dry_run;
    let _sync_mode = config.sync_mode; // TODO: wire up sync mode
    let storage_class = config.storage_class.clone();

    while let Some(info) = list_rx.recv().await {
        // Update discovery progress.
        progress.objects_discovered.fetch_add(1, Ordering::Relaxed);
        progress
            .bytes_discovered
            .fetch_add(info.size, Ordering::Relaxed);

        // Check checkpoint.
        if checkpoint.is_completed(&info.key) {
            let _ = result_tx
                .send(CopyResult {
                    source_key: info.key.clone(),
                    destination_key: String::new(),
                    outcome: CopyOutcome::Skipped {
                        reason: SkipReason::AlreadyCheckpointed,
                    },
                    duration: Duration::ZERO,
                    bytes_copied: 0,
                })
                .await;
            continue;
        }

        // Apply filters.
        if !filter.matches(&info) {
            let _ = result_tx
                .send(CopyResult {
                    source_key: info.key.clone(),
                    destination_key: String::new(),
                    outcome: CopyOutcome::Skipped {
                        reason: SkipReason::FilteredOut,
                    },
                    duration: Duration::ZERO,
                    bytes_copied: 0,
                })
                .await;
            continue;
        }

        // Compute destination key.
        let relative_key = info
            .key
            .strip_prefix(&source_prefix.key)
            .unwrap_or(&info.key);
        let dest_uri = dest.join(relative_key);

        // Dry-run mode.
        if dry_run {
            let _ = result_tx
                .send(CopyResult {
                    source_key: info.key.clone(),
                    destination_key: dest_uri.to_string(),
                    outcome: CopyOutcome::Skipped {
                        reason: SkipReason::DryRun,
                    },
                    duration: Duration::ZERO,
                    bytes_copied: info.size,
                })
                .await;
            continue;
        }

        // Acquire semaphore permit and spawn copy task.
        let permit = semaphore.clone().acquire_owned().await.unwrap();
        let result_tx = result_tx.clone();
        let s3 = s3.clone();
        let concurrency_ctrl = concurrency_ctrl.clone();
        let semaphore = semaphore.clone();
        let active_permits = active_permits.clone();
        let storage_class = storage_class.clone();
        let source_uri = S3Uri {
            bucket: source_prefix.bucket.clone(),
            key: info.key.clone(),
        };

        tokio::spawn(async move {
            let copy_start = Instant::now();
            let strategy = CopyStrategy::for_size(info.size);

            let outcome = copy_with_retry(
                &s3,
                &source_uri,
                &dest_uri,
                info.size,
                &strategy,
                storage_class.as_deref(),
                &concurrency_ctrl,
                &semaphore,
                &active_permits,
            )
            .await;

            let _ = result_tx
                .send(CopyResult {
                    source_key: source_uri.key,
                    destination_key: dest_uri.to_string(),
                    outcome,
                    duration: copy_start.elapsed(),
                    bytes_copied: info.size,
                })
                .await;

            drop(permit);
        });
    }

    // Drop our sender so the collector can finish.
    drop(result_tx);

    // Wait for listing and collection to complete.
    let total_listed = list_handle.await.unwrap_or(Ok(0))?;
    collector_handle.await.unwrap();

    // Flush checkpoint.
    checkpoint.flush()?;

    let manifest = CopyManifest {
        total_objects: total_listed,
        total_bytes: progress.bytes_discovered.load(Ordering::Relaxed),
        copied_objects: copied_objects.load(Ordering::Relaxed),
        copied_bytes: copied_bytes.load(Ordering::Relaxed),
        skipped_objects: skipped_objects.load(Ordering::Relaxed),
        failed_objects: failed_objects.load(Ordering::Relaxed),
        duration_secs: start.elapsed().as_secs_f64(),
        failed_keys: match Arc::try_unwrap(failed_keys) {
            Ok(mutex) => mutex.into_inner(),
            Err(arc) => arc.blocking_lock().clone(),
        },
    };

    info!(
        copied = manifest.copied_objects,
        skipped = manifest.skipped_objects,
        failed = manifest.failed_objects,
        duration_secs = format!("{:.2}", manifest.duration_secs),
        "copy session complete"
    );

    Ok(manifest)
}

/// Copy a single object with exponential backoff retry on retryable errors.
#[allow(clippy::too_many_arguments)]
async fn copy_with_retry(
    s3: &Arc<dyn S3Operations>,
    source: &S3Uri,
    destination: &S3Uri,
    object_size: u64,
    strategy: &CopyStrategy,
    storage_class: Option<&str>,
    concurrency_ctrl: &AdaptiveConcurrency,
    semaphore: &Semaphore,
    active_permits: &AtomicUsize,
) -> CopyOutcome {
    let mut last_error = String::new();

    for attempt in 0..=MAX_RETRIES {
        let result = match strategy {
            CopyStrategy::ServerSideSingle => {
                s3.copy_object(source, destination, storage_class).await
            }
            CopyStrategy::ServerSideMultipart { part_size } => {
                s3.copy_object_multipart(
                    source,
                    destination,
                    object_size,
                    *part_size,
                    storage_class,
                )
                .await
            }
        };

        match result {
            Ok(e_tag) => {
                if let Some(new_target) = concurrency_ctrl.on_success() {
                    resize_semaphore(semaphore, active_permits, new_target as usize);
                }
                return CopyOutcome::Success { dest_e_tag: e_tag };
            }
            Err(e) => {
                last_error = e.to_string();

                if e.is_retryable() {
                    if let Some(new_target) = concurrency_ctrl.on_throttle() {
                        resize_semaphore(semaphore, active_permits, new_target as usize);
                    }
                }

                if !e.is_retryable() || attempt == MAX_RETRIES {
                    return CopyOutcome::Failed {
                        error: last_error,
                        retries_exhausted: attempt == MAX_RETRIES,
                    };
                }

                // Exponential backoff with full jitter.
                let base_backoff = INITIAL_BACKOFF * 2u32.saturating_pow(attempt);
                let capped = base_backoff.min(MAX_BACKOFF);
                let jitter =
                    Duration::from_millis((rand_jitter() * capped.as_millis() as f64) as u64);
                let sleep_duration = jitter.min(capped);

                debug!(
                    attempt = attempt + 1,
                    max = MAX_RETRIES,
                    backoff_ms = sleep_duration.as_millis() as u64,
                    key = %source.key,
                    error = %last_error,
                    "retrying copy after backoff"
                );

                tokio::time::sleep(sleep_duration).await;
            }
        }
    }

    CopyOutcome::Failed {
        error: last_error,
        retries_exhausted: true,
    }
}

/// Resize the semaphore to match a new target permit count.
///
/// Uses atomic tracking of the current permit count to add or forget permits.
fn resize_semaphore(semaphore: &Semaphore, active_permits: &AtomicUsize, new_target: usize) {
    let old = active_permits.load(Ordering::SeqCst);
    if new_target > old {
        let to_add = new_target - old;
        semaphore.add_permits(to_add);
        active_permits.store(new_target, Ordering::SeqCst);
        debug!(old, new = new_target, added = to_add, "semaphore expanded");
    } else if new_target < old {
        let to_remove = old - new_target;
        semaphore.forget_permits(to_remove);
        active_permits.store(new_target, Ordering::SeqCst);
        debug!(
            old,
            new = new_target,
            removed = to_remove,
            "semaphore shrunk"
        );
    }
}

/// Simple pseudo-random jitter in [0.0, 1.0) using time-based seed.
fn rand_jitter() -> f64 {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .subsec_nanos();
    (nanos % 1000) as f64 / 1000.0
}
