"""
S3-to-S3 file copying service for append operations.

Provides efficient S3-to-S3 copy operations without downloading files locally.
Uses dependency injection and follows CLAUDE.md single responsibility principles.
"""

from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import random
import boto3
from botocore.exceptions import ClientError
from .manifest_reader import S3FileInfo


@dataclass
class CopyResult:
    """Result of S3 file copy operation."""
    source_key: str
    target_key: str
    success: bool
    error_message: Optional[str] = None
    size_bytes: int = 0


class S3FileCopier:
    """
    Service for S3-to-S3 file copy operations.
    
    Single responsibility: Copy files between S3 locations efficiently.
    Uses dependency injection for S3 client and configuration.
    """
    
    def __init__(self, s3_client, bucket: str, max_workers: int = 5):
        """
        Initialize with explicit dependencies.
        
        Args:
            s3_client: Boto3 S3 client for operations
            bucket: S3 bucket name (source and target must be in same bucket)
            max_workers: Maximum number of parallel copy operations
        """
        self.s3_client = s3_client
        self.bucket = bucket
        self.max_workers = max_workers
    
    def copy_files_to_drop(
        self, 
        source_files: List[S3FileInfo], 
        target_drop_prefix: str,
        skip_existing: bool = True
    ) -> List[CopyResult]:
        """
        Copy multiple files to a new drop folder.
        
        Performs S3-to-S3 copy operations for all files in the list,
        maintaining the original filename structure. Includes smart
        copy detection to skip files that already exist.
        
        Args:
            source_files: List of source files to copy
            target_drop_prefix: Target drop S3 prefix (e.g., "billing-data/org/provider/period/drop_id/")
            skip_existing: Whether to skip files that already exist in target
            
        Returns:
            List of CopyResult objects indicating success/failure for each file
        """
        results = []
        
        print(f"📋 Copying {len(source_files)} existing files to new drop...")
        if skip_existing:
            print(f"    🧠 Smart copy detection enabled - skipping existing files")
        
        for file_info in source_files:
            target_key = f"{target_drop_prefix}{file_info.filename}"
            
            # Check if target file already exists and skip if requested
            if skip_existing and self._file_exists(target_key):
                results.append(CopyResult(
                    source_key=file_info.key,
                    target_key=target_key,
                    success=True,
                    size_bytes=file_info.size_bytes
                ))
                print(f"    ⏭️  Skipped {file_info.filename} (already exists)")
                continue
            
            try:
                # Perform S3-to-S3 copy operation
                copy_source = {
                    'Bucket': self.bucket,
                    'Key': file_info.key
                }
                
                self.s3_client.copy_object(
                    CopySource=copy_source,
                    Bucket=self.bucket,
                    Key=target_key
                )
                
                results.append(CopyResult(
                    source_key=file_info.key,
                    target_key=target_key,
                    success=True,
                    size_bytes=file_info.size_bytes
                ))
                
                print(f"    ✓ Copied {file_info.filename} ({file_info.size_bytes / (1024*1024):.2f}MB)")
                
            except ClientError as e:
                error_msg = f"S3 copy failed: {str(e)}"
                results.append(CopyResult(
                    source_key=file_info.key,
                    target_key=target_key,
                    success=False,
                    error_message=error_msg
                ))
                
                print(f"    ❌ Failed to copy {file_info.filename}: {error_msg}")
        
        successful_copies = sum(1 for r in results if r.success)
        total_size_mb = sum(r.size_bytes for r in results if r.success) / (1024 * 1024)
        actual_copies = sum(1 for r in results if r.success and not (skip_existing and self._file_exists(r.target_key)))
        skipped_copies = len(source_files) - len([r for r in results if not r.success]) - actual_copies if skip_existing else 0
        
        if skip_existing and skipped_copies > 0:
            print(f"📋 Copy complete: {actual_copies} copied + {skipped_copies} skipped = {successful_copies}/{len(source_files)} files ({total_size_mb:.2f}MB)")
        else:
            print(f"📋 Copy complete: {successful_copies}/{len(source_files)} files ({total_size_mb:.2f}MB)")
        
        return results
    
    def copy_files_parallel(
        self, 
        source_files: List[S3FileInfo], 
        target_drop_prefix: str,
        skip_existing: bool = True
    ) -> List[CopyResult]:
        """
        Copy multiple files to new drop using parallel operations.
        
        Uses ThreadPoolExecutor for concurrent S3-to-S3 copy operations
        to improve performance for large numbers of files. Includes smart
        copy detection to skip files that already exist in the target.
        
        Args:
            source_files: List of source files to copy
            target_drop_prefix: Target drop S3 prefix
            skip_existing: Whether to skip files that already exist in target
            
        Returns:
            List of CopyResult objects indicating success/failure for each file
        """
        if not source_files:
            return []
        
        print(f"📋 Copying {len(source_files)} files in parallel (max {self.max_workers} workers)...")
        if skip_existing:
            print(f"    🧠 Smart copy detection enabled - skipping existing files")
        start_time = time.time()
        
        # Filter out files that already exist if skip_existing is True
        files_to_copy = source_files
        if skip_existing:
            files_to_copy = self._filter_files_needing_copy(source_files, target_drop_prefix)
            if len(files_to_copy) < len(source_files):
                skipped_count = len(source_files) - len(files_to_copy)
                print(f"    ⏭️  Skipping {skipped_count} files that already exist in target")
        
        if not files_to_copy:
            print(f"📋 No files need copying - all {len(source_files)} files already exist in target")
            # Create success results for all skipped files
            return [CopyResult(
                source_key=file_info.key,
                target_key=f"{target_drop_prefix}{file_info.filename}",
                success=True,
                size_bytes=file_info.size_bytes
            ) for file_info in source_files]
        
        results = []
        
        # Use ThreadPoolExecutor for parallel copying
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            # Submit all copy tasks
            future_to_file = {
                executor.submit(
                    self.copy_single_file,
                    file_info.key,
                    f"{target_drop_prefix}{file_info.filename}",
                    file_info.size_bytes,
                    skip_existing=False  # Already filtered above
                ): file_info
                for file_info in files_to_copy
            }
            
            # Collect results as they complete
            completed_count = 0
            for future in as_completed(future_to_file):
                file_info = future_to_file[future]
                try:
                    copy_result = future.result()
                    results.append(copy_result)
                    
                    completed_count += 1
                    if copy_result.success:
                        size_mb = copy_result.size_bytes / (1024 * 1024)
                        print(f"    ✓ [{completed_count}/{len(files_to_copy)}] {file_info.filename} ({size_mb:.2f}MB)")
                    else:
                        print(f"    ❌ [{completed_count}/{len(files_to_copy)}] {file_info.filename}: {copy_result.error_message}")
                        
                except Exception as e:
                    # Handle unexpected errors
                    results.append(CopyResult(
                        source_key=file_info.key,
                        target_key=f"{target_drop_prefix}{file_info.filename}",
                        success=False,
                        error_message=f"Unexpected error: {str(e)}"
                    ))
                    completed_count += 1
                    print(f"    ❌ [{completed_count}/{len(files_to_copy)}] {file_info.filename}: Unexpected error: {str(e)}")
        
        # Add success results for skipped files
        if skip_existing:
            skipped_files = [f for f in source_files if f not in files_to_copy]
            for file_info in skipped_files:
                results.append(CopyResult(
                    source_key=file_info.key,
                    target_key=f"{target_drop_prefix}{file_info.filename}",
                    success=True,
                    size_bytes=file_info.size_bytes
                ))
        
        # Summary
        successful_copies = sum(1 for r in results if r.success)
        total_size_mb = sum(r.size_bytes for r in results if r.success) / (1024 * 1024)
        elapsed_time = time.time() - start_time
        
        actual_copies = len(files_to_copy)
        skipped_copies = len(source_files) - len(files_to_copy) if skip_existing else 0
        
        if skip_existing and skipped_copies > 0:
            print(f"📋 Parallel copy complete: {actual_copies} copied + {skipped_copies} skipped = {successful_copies}/{len(source_files)} files ({total_size_mb:.2f}MB) in {elapsed_time:.1f}s")
        else:
            print(f"📋 Parallel copy complete: {successful_copies}/{len(source_files)} files ({total_size_mb:.2f}MB) in {elapsed_time:.1f}s")
        
        return results
    
    def copy_single_file(
        self, 
        source_key: str, 
        target_key: str,
        size_bytes: Optional[int] = None,
        max_retries: int = 3,
        skip_existing: bool = True
    ) -> CopyResult:
        """
        Copy a single file between S3 locations with retry logic.
        
        Performs S3-to-S3 copy operation with exponential backoff retry
        for transient failures (throttling, network issues, etc.). Includes
        smart copy detection to skip files that already exist.
        
        Args:
            source_key: Source S3 key
            target_key: Target S3 key
            size_bytes: Optional file size for reporting
            max_retries: Maximum number of retry attempts
            skip_existing: Whether to skip if target file already exists
            
        Returns:
            CopyResult indicating success or failure
        """
        # Check if target file already exists and skip if requested
        if skip_existing and self._file_exists(target_key):
            return CopyResult(
                source_key=source_key,
                target_key=target_key,
                success=True,
                size_bytes=size_bytes or 0
            )
        
        last_error = None
        
        for attempt in range(max_retries + 1):
            try:
                copy_source = {
                    'Bucket': self.bucket,
                    'Key': source_key
                }
                
                self.s3_client.copy_object(
                    CopySource=copy_source,
                    Bucket=self.bucket,
                    Key=target_key
                )
                
                return CopyResult(
                    source_key=source_key,
                    target_key=target_key,
                    success=True,
                    size_bytes=size_bytes or 0
                )
                
            except ClientError as e:
                last_error = e
                error_code = e.response.get('Error', {}).get('Code', '')
                
                # Check if this is a retryable error
                if self._is_retryable_error(error_code) and attempt < max_retries:
                    # Exponential backoff with jitter
                    wait_time = (2 ** attempt) + random.uniform(0, 1)
                    time.sleep(wait_time)
                    continue
                else:
                    # Non-retryable error or max retries exceeded
                    break
            
            except Exception as e:
                # Unexpected error - don't retry
                last_error = e
                break
        
        # All retries failed
        return CopyResult(
            source_key=source_key,
            target_key=target_key,
            success=False,
            error_message=f"S3 copy failed after {max_retries} retries: {str(last_error)}"
        )
    
    def _is_retryable_error(self, error_code: str) -> bool:
        """
        Determine if an S3 error is worth retrying.
        
        Args:
            error_code: AWS error code from ClientError
            
        Returns:
            True if error is likely transient and worth retrying
        """
        retryable_errors = {
            'ThrottlingException',
            'Throttling', 
            'ServiceUnavailable',
            'InternalError',
            'RequestTimeout',
            'SlowDown'
        }
        
        return error_code in retryable_errors
    
    def validate_copy_requirements(
        self, 
        source_files: List[S3FileInfo], 
        target_drop_prefix: str
    ) -> Dict[str, Any]:
        """
        Validate that copy operation can proceed.
        
        Checks for potential conflicts and validates prerequisites
        for the copy operation.
        
        Args:
            source_files: Files to be copied
            target_drop_prefix: Target location
            
        Returns:
            Validation results dict with 'valid' boolean and 'issues' list
        """
        issues = []
        
        # Check if source files exist and are accessible
        if not source_files:
            issues.append("No source files to copy")
        
        # Check for potential filename conflicts in target location
        try:
            existing_files = self._list_existing_files_in_prefix(target_drop_prefix)
            source_filenames = {f.filename for f in source_files}
            existing_filenames = {f.split('/')[-1] for f in existing_files}
            
            conflicts = source_filenames.intersection(existing_filenames)
            if conflicts:
                issues.append(f"Filename conflicts detected: {', '.join(conflicts)}")
                
        except ClientError as e:
            # Target location might not exist yet, which is fine
            if e.response['Error']['Code'] != 'NoSuchKey':
                issues.append(f"Could not validate target location: {str(e)}")
        
        return {
            'valid': len(issues) == 0,
            'issues': issues
        }
    
    def _filter_files_needing_copy(
        self, 
        source_files: List[S3FileInfo], 
        target_drop_prefix: str
    ) -> List[S3FileInfo]:
        """
        Filter source files to only include those that need copying.
        
        Checks which files already exist in the target location and
        filters them out to avoid unnecessary copy operations.
        
        Args:
            source_files: List of source files to check
            target_drop_prefix: Target drop prefix to check against
            
        Returns:
            Filtered list of files that need copying
        """
        if not source_files:
            return []
        
        try:
            # Get list of existing files in target location
            existing_files = self._list_existing_files_in_prefix(target_drop_prefix)
            existing_filenames = {key.split('/')[-1] for key in existing_files}
            
            # Filter out files that already exist
            files_needing_copy = [
                file_info for file_info in source_files 
                if file_info.filename not in existing_filenames
            ]
            
            return files_needing_copy
            
        except ClientError:
            # If we can't check, assume all files need copying
            return source_files
    
    def _file_exists(self, key: str) -> bool:
        """
        Check if a file exists in S3.
        
        Args:
            key: S3 key to check
            
        Returns:
            True if file exists, False otherwise
        """
        try:
            self.s3_client.head_object(Bucket=self.bucket, Key=key)
            return True
        except ClientError as e:
            error_code = e.response.get('Error', {}).get('Code', '')
            if error_code == '404':
                return False
            else:
                # For other errors, assume file doesn't exist to be safe
                return False
    
    def _list_existing_files_in_prefix(self, prefix: str) -> List[str]:
        """List existing files in a given S3 prefix."""
        try:
            response = self.s3_client.list_objects_v2(
                Bucket=self.bucket,
                Prefix=prefix
            )
            
            if 'Contents' in response:
                return [obj['Key'] for obj in response['Contents']]
            else:
                return []
                
        except ClientError:
            return []