#!/usr/bin/env python3
"""
Parallel S3 upload script with progress tracking.
Efficiently uploads large numbers of files to S3.
"""

import os
import boto3
from botocore.config import Config
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from tqdm import tqdm
import logging
from botocore.exceptions import ClientError


# Setup logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('s3_upload.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)


def upload_file(args):
    """
    Upload a single file to S3.
    
    Args:
        args: tuple of (local_path, s3_client, bucket, s3_key, check_exists)
    
    Returns:
        tuple: (success: bool, local_path: str, error_message: str or None)
    """
    local_path, s3_client, bucket, s3_key, check_exists = args
    
    try:
        # Check if file already exists in S3 (optional optimization)
        if check_exists:
            try:
                s3_client.head_object(Bucket=bucket, Key=s3_key)
                # File exists and is accessible, skip upload
                return True, local_path, None
            except ClientError as e:
                if e.response['Error']['Code'] != '404':
                    # Some other error occurred
                    return False, local_path, f"Error checking file: {str(e)}"
                # File doesn't exist, proceed with upload
        
        # Upload the file
        s3_client.upload_file(
            local_path,
            bucket,
            s3_key,
            ExtraArgs={'ContentType': 'audio/opus'}  # Set content type for opus files
        )
        return True, local_path, None
        
    except Exception as e:
        return False, local_path, str(e)


def get_all_files(directory):
    """
    Recursively get all files in a directory using os.walk (faster for large dirs).
    
    Args:
        directory: Path to scan
        
    Yields:
        Path objects for each file
    """
    for root, dirs, files in os.walk(directory):
        for filename in files:
            yield Path(root) / filename


def main():
    # Configuration
    local_dir = "/app2/suno/data/sara/sfx_get_beats/opus_audio_truncated"
    s3_bucket = "suno-data"
    s3_prefix = "datasets/bundles/v5/sfx_all_processed/opus_audio_truncated"
    
    # Performance settings
    max_workers = 32  # Number of parallel uploads (adjust based on your network)
    check_exists = True  # Set to False to skip existence checks and always upload
    
    logger.info(f"Starting parallel S3 upload")
    logger.info(f"Local directory: {local_dir}")
    logger.info(f"S3 destination: s3://{s3_bucket}/{s3_prefix}")
    logger.info(f"Max workers: {max_workers}")
    logger.info(f"Check if files exist: {check_exists}")
    
    # Initialize S3 client with larger connection pool
    # Connection pool must be >= max_workers to avoid "Connection pool is full" errors
    config = Config(
        max_pool_connections=max_workers * 2,  # 2x workers for safety
        retries={'max_attempts': 3, 'mode': 'adaptive'}
    )
    s3_client = boto3.client('s3', config=config)
    
    # Start scanning and uploading immediately (streaming approach)
    logger.info("Starting file discovery and upload...")
    local_dir_path = Path(local_dir)
    
    successful = 0
    failed = 0
    skipped = 0
    errors = []
    files_processed = 0
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {}
        
        # Progress bar without total (will update as we go)
        with tqdm(desc="Uploading", unit="files") as pbar:
            # Start discovering and submitting files
            for file_path in get_all_files(local_dir):
                # Calculate relative path and S3 key
                relative_path = file_path.relative_to(local_dir_path)
                s3_key = f"{s3_prefix}/{relative_path}".replace('\\', '/')  # Handle Windows paths
                
                upload_task = (
                    str(file_path),
                    s3_client,
                    s3_bucket,
                    s3_key,
                    check_exists
                )
                
                # Submit task
                future = executor.submit(upload_file, upload_task)
                futures[future] = upload_task
                
                # Process completed uploads while discovering more files
                # This prevents memory buildup from too many pending futures
                while len(futures) > max_workers * 10:  # Keep queue manageable
                    done_futures = [f for f in futures if f.done()]
                    for future in done_futures:
                        success, local_path, error = future.result()
                        
                        if success:
                            if error is None:
                                successful += 1
                            else:
                                skipped += 1
                        else:
                            failed += 1
                            errors.append((local_path, error))
                            logger.error(f"Failed to upload {local_path}: {error}")
                        
                        files_processed += 1
                        pbar.update(1)
                        pbar.set_postfix({
                            'success': successful,
                            'skipped': skipped,
                            'failed': failed,
                            'queued': len(futures)
                        })
                        del futures[future]
            
            # Process remaining futures
            logger.info("Finishing remaining uploads...")
            for future in as_completed(futures):
                success, local_path, error = future.result()
                
                if success:
                    if error is None:
                        successful += 1
                    else:
                        skipped += 1
                else:
                    failed += 1
                    errors.append((local_path, error))
                    logger.error(f"Failed to upload {local_path}: {error}")
                
                files_processed += 1
                pbar.update(1)
                pbar.set_postfix({
                    'success': successful,
                    'skipped': skipped,
                    'failed': failed
                })
    
    # Summary
    logger.info("\n" + "="*80)
    logger.info("Upload Summary:")
    logger.info(f"Total files processed: {files_processed:,}")
    logger.info(f"Successfully uploaded: {successful:,}")
    logger.info(f"Skipped (already exist): {skipped:,}")
    logger.info(f"Failed: {failed:,}")
    logger.info("="*80)
    
    # Save errors to file
    if errors:
        error_file = "s3_upload_errors.txt"
        logger.info(f"\nWriting errors to {error_file}...")
        with open(error_file, 'w') as f:
            for local_path, error in errors:
                f.write(f"{local_path}\t{error}\n")
        logger.info(f"Errors saved to {error_file}")
    
    logger.info("\nUpload complete!")


if __name__ == "__main__":
    main()

