"""
S3 cleanup utilities for IICY deployment bucket management.

This module provides safe utilities for cleaning up S3 buckets created during
Lambda deployment processes.
"""

import boto3
from typing import Optional, List
from botocore.exceptions import ClientError
import click


class IICYBucketCleanupError(Exception):
    """Raised when S3 bucket cleanup fails in a recoverable way."""
    pass


def empty_and_delete_bucket(s3_client, bucket_name: str) -> None:
    """
    Safely empty and delete an S3 bucket.
    
    This function handles versioned buckets by deleting all object versions
    and delete markers before deleting the bucket itself.
    
    Args:
        s3_client: Boto3 S3 client instance
        bucket_name: Name of the bucket to delete
        
    Raises:
        IICYBucketCleanupError: If bucket cleanup fails in a recoverable way
    """
    try:
        click.echo(f"   Deleting deployment bucket '{bucket_name}'...")
        
        # First check if bucket exists
        try:
            s3_client.head_bucket(Bucket=bucket_name)
        except ClientError as e:
            if e.response['Error']['Code'] in ['404', 'NoSuchBucket']:
                click.echo(f"   Bucket '{bucket_name}' does not exist - skipping")
                return
            else:
                raise
        
        # Verify this looks like an IICY deployment bucket
        if not _is_deployment_bucket(s3_client, bucket_name):
            click.echo(f"   Bucket '{bucket_name}' doesn't appear to be an IICY deployment bucket - skipping")
            return
        
        # Empty the bucket (handle versioned objects)
        _empty_bucket(s3_client, bucket_name)
        
        # Delete the bucket
        s3_client.delete_bucket(Bucket=bucket_name)
        click.echo(f"   ✅ Successfully deleted deployment bucket '{bucket_name}'")
        
    except ClientError as e:
        error_code = e.response.get('Error', {}).get('Code', 'Unknown')
        error_message = e.response.get('Error', {}).get('Message', str(e))
        
        # Handle specific AWS errors gracefully
        if error_code == 'NoSuchBucket':
            click.echo(f"   Bucket '{bucket_name}' already deleted - skipping")
            return
        elif error_code == 'BucketNotEmpty':
            raise IICYBucketCleanupError(
                f"Bucket '{bucket_name}' still contains objects after emptying attempt"
            )
        elif error_code == 'AccessDenied':
            raise IICYBucketCleanupError(
                f"Access denied when deleting bucket '{bucket_name}': {error_message}"
            )
        else:
            raise IICYBucketCleanupError(
                f"Failed to delete bucket '{bucket_name}': {error_code} - {error_message}"
            )
    except Exception as e:
        raise IICYBucketCleanupError(
            f"Unexpected error deleting bucket '{bucket_name}': {str(e)}"
        )


def _is_deployment_bucket(s3_client, bucket_name: str) -> bool:
    """
    Verify that a bucket appears to be an IICY deployment bucket.
    
    This adds an extra safety check by looking for Lambda deployment artifacts
    in the bucket.
    
    Args:
        s3_client: Boto3 S3 client instance
        bucket_name: Name of the bucket to check
        
    Returns:
        True if the bucket appears to be an IICY deployment bucket
    """
    try:
        # Check if bucket has objects with lambda/ prefix
        response = s3_client.list_objects_v2(
            Bucket=bucket_name,
            Prefix='lambda/',
            MaxKeys=1
        )
        
        # If we find any objects with lambda/ prefix, it's likely our bucket
        has_lambda_objects = response.get('Contents', [])
        
        if has_lambda_objects:
            return True
        
        # Also check if bucket is empty (newly created deployment bucket)
        response = s3_client.list_objects_v2(Bucket=bucket_name, MaxKeys=1)
        is_empty = not response.get('Contents', [])
        
        # Empty bucket with deployment naming pattern is also likely ours
        if is_empty and bucket_name.endswith('-deployment-bucket'):
            return True
            
        return False
        
    except ClientError:
        # If we can't list objects, assume it's not our bucket
        return False


def _empty_bucket(s3_client, bucket_name: str) -> None:
    """
    Empty all objects and versions from an S3 bucket.
    
    This handles both regular objects and versioned objects/delete markers.
    
    Args:
        s3_client: Boto3 S3 client instance
        bucket_name: Name of the bucket to empty
    """
    click.echo(f"      Emptying bucket '{bucket_name}'...")
    
    # Delete all object versions and delete markers
    paginator = s3_client.get_paginator('list_object_versions')
    page_iterator = paginator.paginate(Bucket=bucket_name)
    
    objects_deleted = 0
    
    for page in page_iterator:
        # Collect versions to delete
        delete_list = []
        
        # Add all object versions
        for version in page.get('Versions', []):
            delete_list.append({
                'Key': version['Key'],
                'VersionId': version['VersionId']
            })
        
        # Add all delete markers
        for marker in page.get('DeleteMarkers', []):
            delete_list.append({
                'Key': marker['Key'],
                'VersionId': marker['VersionId']
            })
        
        # Delete in batches (max 1000 per request)
        if delete_list:
            # Split into chunks of 1000
            for i in range(0, len(delete_list), 1000):
                chunk = delete_list[i:i+1000]
                s3_client.delete_objects(
                    Bucket=bucket_name,
                    Delete={'Objects': chunk}
                )
                objects_deleted += len(chunk)
    
    if objects_deleted > 0:
        click.echo(f"      Deleted {objects_deleted} objects/versions from bucket")
    else:
        click.echo(f"      Bucket was already empty")


def get_deployment_bucket_names(stack_names: List[str]) -> List[str]:
    """
    Generate expected deployment bucket names for given stack names.
    
    Args:
        stack_names: List of CloudFormation stack names
        
    Returns:
        List of expected S3 bucket names for Lambda deployments
    """
    return [f"{stack_name.lower()}-deployment-bucket" for stack_name in stack_names]


def cleanup_deployment_buckets(stack_names: List[str], session: boto3.Session = None) -> None:
    """
    Clean up deployment buckets for the given stack names.
    
    This is the main entry point for cleaning up S3 buckets during stack destruction.
    
    Args:
        stack_names: List of CloudFormation stack names
        session: Optional boto3 session (for role assumption)
    """
    if not stack_names:
        return
    
    if session is None:
        session = boto3.Session()
    
    s3_client = session.client('s3')
    bucket_names = get_deployment_bucket_names(stack_names)
    
    click.echo("Cleaning up Lambda deployment buckets...")
    
    cleanup_failures = []
    
    for bucket_name in bucket_names:
        try:
            empty_and_delete_bucket(s3_client, bucket_name)
        except IICBucketCleanupError as e:
            click.echo(f"   ❌ Failed to delete bucket '{bucket_name}': {e} (continuing)", err=True)
            cleanup_failures.append(bucket_name)
        except Exception as e:
            click.echo(f"   ❌ Unexpected error deleting bucket '{bucket_name}': {e} (continuing)", err=True)
            cleanup_failures.append(bucket_name)
    
    if cleanup_failures:
        click.echo(f"   ⚠️  {len(cleanup_failures)} bucket(s) could not be cleaned up:", err=True)
        for bucket_name in cleanup_failures:
            click.echo(f"      - {bucket_name}", err=True)
        click.echo("   You may need to manually delete these buckets.", err=True)
    else:
        successful_count = len([b for b in bucket_names if b not in cleanup_failures])
        if successful_count > 0:
            click.echo(f"   ✅ Successfully cleaned up {successful_count} deployment bucket(s)")