"""
Append upload orchestrator for coordinating S3-to-S3 copy + upload workflow.

Extends existing upload workflow with efficient file copying from source drops.
Uses dependency injection and follows CLAUDE.md single responsibility principles.
"""

from typing import Dict, List, Any, Optional
from dataclasses import dataclass
import boto3

from .manifest_reader import ManifestReader, DropInfo, S3FileInfo
from .s3_file_copier import S3FileCopier, CopyResult
from .path_manager import PathManager, PathTranslation
from .append_pruning_service import AppendPruningService
from ..models import UploadConfig, OrganizationResult, UploadResult
from src.services.upload_workflow_methods import UploadWorkflowMethods


@dataclass
class AppendPlan:
    """Plan for append operation on a single provider/month."""
    organization: str
    provider: str
    billing_period: str
    source_drop: Optional[DropInfo]
    source_files: List[S3FileInfo]
    target_drop_id: str
    copy_plan: List[PathTranslation]
    has_existing_data: bool


class AppendUploadOrchestrator:
    """
    Orchestrator for append upload operations.
    
    Coordinates S3-to-S3 copy operations followed by new data upload,
    extending the existing upload workflow with efficient file copying.
    """
    
    def __init__(
        self,
        manifest_reader: ManifestReader,
        s3_file_copier: S3FileCopier,
        path_manager: PathManager,
        upload_workflow: UploadWorkflowMethods,
        s3_formatter,
        pruning_service: Optional[AppendPruningService] = None
    ):
        """
        Initialize with explicit dependencies.
        
        Args:
            manifest_reader: Service for reading manifests and discovering drops
            s3_file_copier: Service for S3-to-S3 copy operations
            path_manager: Service for path translation and conflict resolution
            upload_workflow: Existing upload workflow methods
            s3_formatter: S3 formatting utilities
            pruning_service: Service for cleaning up failed append operations
        """
        self.manifest_reader = manifest_reader
        self.s3_file_copier = s3_file_copier
        self.path_manager = path_manager
        self.upload_workflow = upload_workflow
        self.s3_formatter = s3_formatter
        self.pruning_service = pruning_service
    
    def execute_append_workflow(self, config: UploadConfig, dry_run: bool = False) -> Dict[str, OrganizationResult]:
        """
        Execute complete append workflow with copy + upload operations.
        
        Coordinates the two-phase append process:
        1. Copy existing files from source drops to new drops
        2. Upload new data using existing workflow
        
        Args:
            config: Upload configuration with append_mode=True
            dry_run: Whether to perform dry run validation only
            
        Returns:
            Dictionary mapping organization names to processing results
        """
        if not config.is_append_mode():
            raise ValueError("AppendUploadOrchestrator requires append_mode=True in config")
        
        print(f"🔄 Starting append workflow ({'dry-run' if dry_run else 'live mode'})")
        print(f"   Source discovery: {'manifest.json' if config.uses_manifest_discovery() else config.source_drop_id}")
        
        results = {}
        
        for org in config.organizations:
            print(f"\n🏢 Processing organization: {org.name}")
            
            try:
                # Phase 1: Plan append operations for this organization
                append_plans = self._create_append_plans(config, org)
                
                if not append_plans:
                    print(f"   ⚠️  No append plans created for {org.name} - may indicate no source data")
                    results[org.name] = self._create_empty_org_result(org.name)
                    continue
                
                # Phase 2: Execute append operations
                if dry_run:
                    org_result = self._execute_dry_run_append(org, append_plans)
                else:
                    org_result = self._execute_live_append(org, append_plans, config)
                
                results[org.name] = org_result
                
            except Exception as e:
                print(f"   ❌ Append workflow failed for {org.name}: {str(e)}")
                results[org.name] = OrganizationResult(
                    organization=org.name,
                    status="failed",
                    error_message=f"Append workflow failed: {str(e)}",
                    providers=[]
                )
        
        return results
    
    def _create_append_plans(self, config: UploadConfig, org) -> List[AppendPlan]:
        """
        Create append plans for all provider/month combinations.
        
        Discovers source drops and existing files, then creates copy plans
        for each provider/billing period combination.
        """
        plans = []
        
        # Generate unique target drop ID for this organization
        target_drop_id = self.s3_formatter.generate_drop_id()
        
        for provider in org.providers:
            for month in (org.months or config.months or []):
                # Calculate billing period from month
                billing_period = self._calculate_billing_period_from_month(month)
                
                print(f"   📋 Planning {provider}/{month} (billing period: {billing_period})")
                
                # Discover source drop
                source_drop = self.manifest_reader.find_active_drop(org.name, provider, billing_period)
                
                if not source_drop:
                    print(f"      ℹ️  No source drop found - will create new drop")
                    plans.append(AppendPlan(
                        organization=org.name,
                        provider=provider,
                        billing_period=billing_period,
                        source_drop=None,
                        source_files=[],
                        target_drop_id=target_drop_id,
                        copy_plan=[],
                        has_existing_data=False
                    ))
                    continue
                
                # Discover source files
                source_files = self.manifest_reader.list_drop_files(source_drop)
                print(f"      📁 Found {len(source_files)} existing files to copy")
                
                # Handle empty source drops explicitly
                if not source_files:
                    print(f"      📭 Source drop {source_drop.drop_id} is empty - will create new drop with only new data")
                    plans.append(AppendPlan(
                        organization=org.name,
                        provider=provider,
                        billing_period=billing_period,
                        source_drop=source_drop,
                        source_files=[],
                        target_drop_id=target_drop_id,
                        copy_plan=[],
                        has_existing_data=False
                    ))
                    continue
                
                # Create copy plan for non-empty source drops
                copy_plan = self.path_manager.generate_safe_target_paths(
                    source_files, org.name, provider, billing_period, target_drop_id
                )
                
                plans.append(AppendPlan(
                    organization=org.name,
                    provider=provider,
                    billing_period=billing_period,
                    source_drop=source_drop,
                    source_files=source_files,
                    target_drop_id=target_drop_id,
                    copy_plan=copy_plan,
                    has_existing_data=len(source_files) > 0
                ))
        
        return plans
    
    def _execute_dry_run_append(self, org, append_plans: List[AppendPlan]) -> OrganizationResult:
        """Execute dry-run validation for append operations."""
        print(f"   🔍 Dry-run validation for append operations")
        
        total_files_to_copy = sum(len(plan.source_files) for plan in append_plans)
        providers_with_data = [plan.provider for plan in append_plans if plan.has_existing_data]
        providers_without_data = [plan.provider for plan in append_plans if not plan.has_existing_data]
        
        print(f"   📊 Would copy {total_files_to_copy} existing files")
        if providers_with_data:
            print(f"   📁 Providers with existing data: {providers_with_data}")
        if providers_without_data:
            print(f"   📭 Providers with no existing data (empty drops): {providers_without_data}")
        print(f"   🆕 Target drop ID: {append_plans[0].target_drop_id if append_plans else 'N/A'}")
        
        return OrganizationResult(
            organization=org.name,
            status="success",
            files_uploaded=total_files_to_copy,  # Files that would be copied
            providers=[plan.provider for plan in append_plans]
        )
    
    def _execute_live_append(self, org, append_plans: List[AppendPlan], config: UploadConfig) -> OrganizationResult:
        """Execute live append operations with copy + upload."""
        print(f"   🚀 Executing append operations")
        
        total_copied = 0
        total_uploaded = 0
        failed_operations = []
        all_upload_results = []
        
        # Phase 1: Copy existing files for all plans
        plans_with_data = [plan for plan in append_plans if plan.has_existing_data]
        plans_without_data = [plan for plan in append_plans if not plan.has_existing_data]
        
        if plans_without_data:
            providers_without_data = [plan.provider for plan in plans_without_data]
            print(f"      📭 {len(plans_without_data)} provider(s) with no existing data: {providers_without_data}")
        
        for plan in plans_with_data:
            print(f"      📋 Copying {len(plan.source_files)} files for {plan.provider}")
            
            copy_results = self.s3_file_copier.copy_files_parallel(
                plan.source_files, 
                self.path_manager.build_drop_prefix(
                    plan.organization, plan.provider, plan.billing_period, plan.target_drop_id
                )
            )
            
            # Count successful copies
            successful_copies = sum(1 for r in copy_results if r.success)
            total_copied += successful_copies
            
            # Track failed copies
            failed_copies = [r for r in copy_results if not r.success]
            for failed_copy in failed_copies:
                failed_operations.append({
                    "operation": "copy",
                    "provider": plan.provider,
                    "file": failed_copy.source_key.split('/')[-1],
                    "error": failed_copy.error_message
                })
        
        # Phase 2: Upload new data using existing workflow
        print(f"   📤 Uploading new data using existing workflow")
        
        # Create modified config for upload with target drop IDs
        upload_config = self._create_upload_config_with_target_drops(config, append_plans)
        
        # Use existing workflow for new data upload
        upload_results = self.upload_workflow._execute_organization_processing(
            upload_config, 
            self._create_processing_plan_for_append(upload_config, append_plans),
            dry_run=False
        )
        
        # Aggregate results
        org_upload_result = upload_results.get(org.name)
        if org_upload_result and org_upload_result.is_successful():
            total_uploaded = org_upload_result.files_uploaded
            all_upload_results = org_upload_result.uploaded_files
        elif org_upload_result:
            failed_operations.extend(org_upload_result.failed_files or [])
        
        # Determine overall status with detailed failure reporting
        total_operations = total_copied + total_uploaded
        
        # Categorize failures by operation type
        copy_failures = [f for f in failed_operations if f.get("operation") == "copy"]
        upload_failures = [f for f in failed_operations if f.get("operation") != "copy"]
        
        if not failed_operations:
            status = "success"
            print(f"   ✅ {org.name}: {total_copied} files copied, {total_uploaded} files uploaded")
        elif failed_operations and total_operations > 0:
            status = "partial"
            print(f"   ⚠️  {org.name}: {total_operations} operations completed, {len(failed_operations)} failed")
            
            # Detailed failure breakdown
            if copy_failures:
                print(f"      📋 Copy failures: {len(copy_failures)}")
                for failure in copy_failures[:3]:  # Show first 3 failures
                    print(f"        • {failure['provider']}/{failure['file']}: {failure['error']}")
                if len(copy_failures) > 3:
                    print(f"        ... and {len(copy_failures) - 3} more copy failures")
                    
            if upload_failures:
                print(f"      📤 Upload failures: {len(upload_failures)}")
                for failure in upload_failures[:3]:  # Show first 3 failures  
                    file_name = failure.get('file', 'unknown')
                    error_msg = failure.get('error', str(failure))
                    print(f"        • {file_name}: {error_msg}")
                if len(upload_failures) > 3:
                    print(f"        ... and {len(upload_failures) - 3} more upload failures")
        else:
            status = "failed"
            print(f"   ❌ {org.name}: All operations failed")
            
            # Show failure summary for complete failures
            if copy_failures and upload_failures:
                print(f"      Both copy ({len(copy_failures)}) and upload ({len(upload_failures)}) operations failed")
            elif copy_failures:
                print(f"      All {len(copy_failures)} copy operations failed")
            elif upload_failures:
                print(f"      All {len(upload_failures)} upload operations failed")
        
        # Handle rollback for complete failures
        if status == "failed" and self.pruning_service:
            print(f"      🔄 Attempting rollback of failed append operation...")
            self._rollback_failed_append(org.name, append_plans, failed_operations)
        elif status == "partial" and failed_operations:
            self._suggest_recovery_actions(org.name, copy_failures, upload_failures, append_plans)
        
        return OrganizationResult(
            organization=org.name,
            status=status,
            drop_id=append_plans[0].target_drop_id if append_plans else None,
            files_uploaded=total_operations,
            files_failed=len(failed_operations),
            providers=[plan.provider for plan in append_plans],
            uploaded_files=all_upload_results,
            failed_files=failed_operations
        )
    
    def _suggest_recovery_actions(
        self, 
        org_name: str, 
        copy_failures: List[Dict], 
        upload_failures: List[Dict], 
        append_plans: List[AppendPlan]
    ) -> None:
        """
        Suggest recovery actions for partial failures.
        
        Provides user-friendly guidance for handling different types of failures
        that can occur during append operations.
        """
        print(f"      💡 Recovery suggestions for {org_name}:")
        
        if copy_failures:
            # Analyze copy failure patterns
            s3_errors = [f for f in copy_failures if "S3" in f.get("error", "")]
            permission_errors = [f for f in copy_failures if any(term in f.get("error", "").lower() 
                                for term in ["access", "permission", "forbidden", "unauthorized"])]
            throttle_errors = [f for f in copy_failures if any(term in f.get("error", "").lower() 
                              for term in ["throttle", "slowdown", "limit"])]
            
            if s3_errors:
                print(f"        📋 S3 copy issues: Check bucket permissions and region configuration")
            if permission_errors:
                print(f"        🔐 Permission issues: Verify AWS credentials have S3 copy permissions")
            if throttle_errors:
                print(f"        🐌 Throttling issues: Retry operation later or reduce concurrency")
            if not (s3_errors or permission_errors or throttle_errors):
                print(f"        📋 Copy issues: Re-run append operation to retry failed copies")
        
        if upload_failures:
            print(f"        📤 Upload issues: Check data generation and CBF validation")
            print(f"        📤 Consider running without --append flag to create new drops")
        
        if append_plans:
            target_drop = append_plans[0].target_drop_id
            print(f"        🔄 Partial drop {target_drop} was created - may need cleanup if re-running")
    
    def _rollback_failed_append(
        self, 
        org_name: str, 
        append_plans: List[AppendPlan], 
        failed_operations: List[Dict]
    ) -> None:
        """
        Rollback failed append operation by cleaning up partial drops.
        
        When an append operation completely fails, this method attempts to
        clean up any partial drops that were created during the failed operation.
        """
        if not self.pruning_service or not append_plans:
            return
        
        # Group plans by provider and extract drop IDs to clean up
        providers_to_cleanup = {}
        target_drop_id = append_plans[0].target_drop_id
        
        for plan in append_plans:
            if plan.provider not in providers_to_cleanup:
                providers_to_cleanup[plan.provider] = {
                    'billing_period': plan.billing_period,
                    'drop_ids': []
                }
            providers_to_cleanup[plan.provider]['drop_ids'].append(target_drop_id)
        
        # Attempt cleanup for each provider
        cleanup_successful = True
        for provider, info in providers_to_cleanup.items():
            try:
                print(f"        🧽 Cleaning up failed drop {target_drop_id} for {provider}")
                
                cleanup_result = self.pruning_service.cleanup_failed_append_operations(
                    organization=org_name,
                    provider=provider,
                    billing_period=info['billing_period'],
                    failed_drop_ids=list(set(info['drop_ids']))  # Remove duplicates
                )
                
                if cleanup_result['cleanup_errors']:
                    print(f"        ⚠️  Cleanup had errors: {len(cleanup_result['cleanup_errors'])} issues")
                    for error in cleanup_result['cleanup_errors'][:2]:  # Show first 2 errors
                        print(f"          • {error}")
                    cleanup_successful = False
                else:
                    cleaned_count = cleanup_result['total_cleaned']
                    print(f"        ✅ Successfully cleaned {cleaned_count} drop(s) for {provider}")
                    
            except Exception as e:
                print(f"        ❌ Rollback failed for {provider}: {str(e)}")
                cleanup_successful = False
        
        if cleanup_successful:
            print(f"      ✅ Rollback completed successfully - failed drops cleaned up")
        else:
            print(f"      ⚠️  Rollback completed with some issues - manual cleanup may be needed")
            print(f"        💡 Run: czbg upload generated --config <config> to retry")
    
    def _create_empty_org_result(self, org_name: str) -> OrganizationResult:
        """Create empty result for organization with no append plans."""
        return OrganizationResult(
            organization=org_name,
            status="success",
            files_uploaded=0,
            providers=[]
        )
    
    def _calculate_billing_period_from_month(self, month: str) -> str:
        """Calculate billing period from YYYY-MM format."""
        year, month_num = month.split('-')
        year = int(year)
        month_num = int(month_num)
        
        start_date = f"{year:04d}{month_num:02d}01"
        
        if month_num == 12:
            next_year = year + 1
            next_month = 1
        else:
            next_year = year
            next_month = month_num + 1
        
        end_date = f"{next_year:04d}{next_month:02d}01"
        return f"{start_date}-{end_date}"
    
    def execute_pre_upload_copy_phase(self, config: UploadConfig, org) -> Dict[str, Any]:
        """
        Execute the pre-upload copy phase for append operations.
        
        Discovers source drops and copies existing files to new drop locations
        before the main upload workflow processes new data.
        
        Args:
            config: Upload configuration with append_mode=True
            org: Organization to process
            
        Returns:
            Dictionary with copy results and target drop information
        """
        print(f"📋 Executing pre-upload copy phase for {org.name}")
        
        # Create append plans
        append_plans = self._create_append_plans(config, org)
        
        if not append_plans:
            return {
                'status': 'no_plans',
                'message': 'No append plans created',
                'target_drop_id': None,
                'copied_files': 0
            }
        
        target_drop_id = append_plans[0].target_drop_id
        total_copied = 0
        copy_errors = []
        
        # Execute copy operations for each plan
        for plan in append_plans:
            if not plan.has_existing_data:
                print(f"   ℹ️  {plan.provider}: No existing data to copy")
                continue
                
            print(f"   📋 Copying {len(plan.source_files)} files for {plan.provider}")
            
            # Build target prefix
            target_prefix = self.path_manager.build_drop_prefix(
                plan.organization, plan.provider, plan.billing_period, plan.target_drop_id
            )
            
            # Execute copy operation (disable smart copy detection for append operations)
            copy_results = self.s3_file_copier.copy_files_parallel(
                plan.source_files, target_prefix, skip_existing=False
            )
            
            # Count results
            successful_copies = sum(1 for r in copy_results if r.success)
            failed_copies = [r for r in copy_results if not r.success]
            
            total_copied += successful_copies
            
            for failed_copy in failed_copies:
                copy_errors.append({
                    'provider': plan.provider,
                    'file': failed_copy.source_key.split('/')[-1],
                    'error': failed_copy.error_message
                })
        
        return {
            'status': 'success' if not copy_errors else 'partial',
            'target_drop_id': target_drop_id,
            'copied_files': total_copied,
            'copy_errors': copy_errors,
            'append_plans': append_plans
        }
    
    def _create_upload_config_with_target_drops(self, config: UploadConfig, append_plans: List[AppendPlan]) -> UploadConfig:
        """
        Create modified upload config that will use the target drops from copy phase.
        
        This is a limitation of the current architecture - the existing upload workflow
        generates its own drop IDs. For now, we'll use the existing workflow as-is
        and handle drop ID coordination in a future iteration.
        """
        # For now, return the original config
        # In a complete implementation, we'd need to modify the upload workflow
        # to accept pre-determined drop IDs
        return config
    
    def _create_processing_plan_for_append(self, config: UploadConfig, append_plans: List[AppendPlan]) -> Any:
        """
        Create processing plan that aligns with existing workflow.
        
        This would need to integrate with the validation service's processing plan structure.
        For now, we'll use the existing workflow's plan generation.
        """
        # This is a design limitation - the existing workflow generates its own plans
        # We'd need to refactor the workflow interface to accept external plans
        return None