"""
Cloudinary storage adapter implementation.

This module provides a concrete implementation of the ImageStorageAdapter
interface using Cloudinary's cloud storage and image transformation services.
"""

import logging
import time
import uuid
from datetime import datetime
from typing import BinaryIO, Dict, Any, Optional

import cloudinary
import cloudinary.uploader
import cloudinary.api
from cloudinary.exceptions import Error as CloudinaryError

from .adapters import ImageStorageAdapter
from .config import CloudinaryConfig
from .exceptions import (
    StorageUploadError,
    StorageDeleteError,
    StorageNotFoundError,
    CloudinaryAuthError,
    CloudinaryRateLimitError,
    StorageQuotaError
)

logger = logging.getLogger(__name__)


class CloudinaryStorage(ImageStorageAdapter):
    """
    Cloudinary storage implementation with automatic transformations and retry logic.
    
    This adapter provides cloud storage capabilities using Cloudinary's service,
    including automatic image optimization, transformations, and CDN delivery.
    It includes robust error handling and retry logic for production use.
    
    Features:
    - Automatic image optimization (quality: auto:good, format: auto)
    - Configurable transformations during upload
    - Retry logic for transient network errors
    - Comprehensive error handling and logging
    - Unique filename generation to prevent conflicts
    """
    
    def __init__(self, config: CloudinaryConfig):
        """
        Initialize Cloudinary storage adapter.
        
        Args:
            config: CloudinaryConfig instance with credentials and settings
        """
        self.config = config
        
        # Configure Cloudinary SDK
        cloudinary.config(
            cloud_name=config.cloud_name,
            api_key=config.api_key,
            api_secret=config.api_secret,
            secure=True  # Always use HTTPS
        )
        
        logger.info(
            "Initialized Cloudinary storage",
            extra={
                'cloud_name': config.safe_cloud_name,
                'api_key': config.safe_api_key,
                'folder_prefix': config.safe_folder_prefix
            }
        )
    
    def upload(self, file: BinaryIO, path: str, **kwargs) -> str:
        """
        Upload a file to Cloudinary with automatic optimization.
        
        This method uploads a file to Cloudinary with automatic transformations
        for optimization. It includes retry logic for transient errors and
        generates unique public_ids to prevent conflicts.
        
        Args:
            file: Binary file object to upload
            path: Logical path for organization (e.g., "reviews/property")
            **kwargs: Additional Cloudinary options (transformations, tags, etc.)
            
        Returns:
            str: Cloudinary public_id that can be used with other methods
            
        Raises:
            StorageUploadError: If upload fails after retries
            CloudinaryAuthError: If authentication fails
            CloudinaryRateLimitError: If rate limit is exceeded
            StorageQuotaError: If storage quota is exceeded
        """
        # Generate unique public_id
        timestamp = int(datetime.now().timestamp())
        unique_id = str(uuid.uuid4())[:8]
        
        # Construct folder path
        folder_path = f"{self.config.folder_prefix}/{path.strip('/')}"
        public_id = f"{folder_path}/review-{unique_id}-{timestamp}"
        
        # Default transformations for optimization
        default_transformations = {
            'quality': 'auto:good',
            'format': 'auto',
            'width': 1600,
            'height': 1600,
            'crop': 'limit'  # Only resize if larger than specified dimensions
        }
        
        # Merge with any custom transformations
        transformations = {**default_transformations, **kwargs.get('transformation', {})}
        
        upload_options = {
            'public_id': public_id,
            'transformation': transformations,
            'resource_type': 'image',
            'overwrite': False,  # Prevent accidental overwrites
            **{k: v for k, v in kwargs.items() if k != 'transformation'}
        }
        
        logger.info(
            "Starting Cloudinary upload",
            extra={
                'public_id': public_id,
                'folder_path': folder_path,
                'transformations': transformations
            }
        )
        
        # Retry logic for transient errors
        max_retries = 3
        base_delay = 1  # seconds
        
        for attempt in range(max_retries):
            try:
                start_time = time.time()
                
                # Reset file pointer to beginning
                file.seek(0)
                
                # Upload to Cloudinary
                result = cloudinary.uploader.upload(file, **upload_options)
                
                duration = time.time() - start_time
                
                logger.info(
                    "Cloudinary upload successful",
                    extra={
                        'public_id': result['public_id'],
                        'bytes': result.get('bytes', 0),
                        'format': result.get('format', 'unknown'),
                        'width': result.get('width', 0),
                        'height': result.get('height', 0),
                        'duration_seconds': round(duration, 2),
                        'attempt': attempt + 1,
                        'url': result.get('secure_url', '')
                    }
                )
                
                return result['public_id']
                
            except CloudinaryError as e:
                error_message = str(e)
                
                # Handle specific Cloudinary errors
                if 'Invalid API Key or API Secret' in error_message:
                    raise CloudinaryAuthError(f"Cloudinary authentication failed: {error_message}")
                
                if 'Rate limit' in error_message or 'Too Many Requests' in error_message:
                    retry_after = 60  # Default retry after 60 seconds
                    raise CloudinaryRateLimitError(error_message, retry_after=retry_after)
                
                if 'quota' in error_message.lower() or 'limit exceeded' in error_message.lower():
                    raise StorageQuotaError(f"Cloudinary quota exceeded: {error_message}")
                
                # For other errors, check if retryable
                retryable_errors = [
                    'timeout', 'connection', 'network', 'temporary', 'server error'
                ]
                is_retryable = any(keyword in error_message.lower() for keyword in retryable_errors)
                
                if not is_retryable or attempt == max_retries - 1:
                    logger.error(
                        "Cloudinary upload failed permanently",
                        extra={
                            'public_id': public_id,
                            'error': error_message,
                            'attempt': attempt + 1,
                            'retryable': is_retryable
                        }
                    )
                    raise StorageUploadError(
                        f"Cloudinary upload failed: {error_message}",
                        retryable=is_retryable
                    )
                
                # Wait before retry with exponential backoff
                delay = base_delay * (2 ** attempt)
                logger.warning(
                    "Cloudinary upload failed, retrying",
                    extra={
                        'public_id': public_id,
                        'error': error_message,
                        'attempt': attempt + 1,
                        'retry_delay_seconds': delay
                    }
                )
                time.sleep(delay)
            
            except Exception as e:
                logger.error(
                    "Unexpected error during Cloudinary upload",
                    extra={
                        'public_id': public_id,
                        'error': str(e),
                        'attempt': attempt + 1
                    }
                )
                raise StorageUploadError(
                    f"Unexpected error during upload: {str(e)}",
                    retryable=True
                )
        
        # This should never be reached due to the loop logic above
        raise StorageUploadError("Upload failed after all retry attempts", retryable=False)
    
    def get_url(self, identifier: str, **kwargs) -> str:
        """
        Generate a URL for accessing the stored image.
        
        This method generates a Cloudinary CDN URL with optional transformations.
        The URL is always HTTPS and includes any requested transformations.
        
        Args:
            identifier: Cloudinary public_id returned by upload()
            **kwargs: Transformation options (width, height, quality, format, etc.)
            
        Returns:
            str: Full HTTPS CDN URL to access the image
            
        Raises:
            StorageNotFoundError: If public_id doesn't exist
        """
        try:
            # Check if resource exists (this also validates the public_id format)
            if not self.exists(identifier):
                raise StorageNotFoundError(f"Cloudinary resource not found: {identifier}")
            
            # Generate URL with transformations
            url, _ = cloudinary.utils.cloudinary_url(
                identifier,
                secure=True,  # Always use HTTPS
                **kwargs
            )
            
            return url
            
        except CloudinaryError as e:
            raise StorageNotFoundError(f"Failed to generate URL for {identifier}: {str(e)}")
    
    def delete(self, identifier: str) -> bool:
        """
        Delete an image from Cloudinary.
        
        This method removes an image from Cloudinary storage. It's idempotent -
        attempting to delete a non-existent image returns False without error.
        
        Args:
            identifier: Cloudinary public_id returned by upload()
            
        Returns:
            bool: True if image was deleted, False if image didn't exist
            
        Raises:
            StorageDeleteError: If deletion fails due to API issues
        """
        try:
            logger.info(
                "Starting Cloudinary delete",
                extra={'public_id': identifier}
            )
            
            result = cloudinary.uploader.destroy(identifier)
            
            # Cloudinary returns 'ok' for successful deletion, 'not found' if doesn't exist
            success = result.get('result') == 'ok'
            not_found = result.get('result') == 'not found'
            
            if success:
                logger.info(
                    "Cloudinary delete successful",
                    extra={'public_id': identifier}
                )
                return True
            elif not_found:
                logger.info(
                    "Cloudinary delete - resource not found",
                    extra={'public_id': identifier}
                )
                return False
            else:
                logger.warning(
                    "Cloudinary delete returned unexpected result",
                    extra={
                        'public_id': identifier,
                        'result': result.get('result', 'unknown')
                    }
                )
                return False
                
        except CloudinaryError as e:
            logger.error(
                "Cloudinary delete failed",
                extra={
                    'public_id': identifier,
                    'error': str(e)
                }
            )
            raise StorageDeleteError(f"Failed to delete {identifier}: {str(e)}")
    
    def exists(self, identifier: str) -> bool:
        """
        Check if an image exists in Cloudinary.
        
        Args:
            identifier: Cloudinary public_id returned by upload()
            
        Returns:
            bool: True if image exists, False otherwise
        """
        try:
            cloudinary.api.resource(identifier)
            return True
        except CloudinaryError as e:
            # Cloudinary raises an exception for not found resources
            if 'Not Found' in str(e) or 'Resource not found' in str(e):
                return False
            # For other errors, log and return False
            logger.warning(
                "Error checking Cloudinary resource existence",
                extra={
                    'public_id': identifier,
                    'error': str(e)
                }
            )
            return False
    
    def get_metadata(self, identifier: str) -> Dict[str, Any]:
        """
        Retrieve metadata for a stored image.
        
        This method returns comprehensive metadata from Cloudinary including
        file size, dimensions, format, upload date, and other properties.
        
        Args:
            identifier: Cloudinary public_id returned by upload()
            
        Returns:
            dict: Metadata dictionary with image information
            
        Raises:
            StorageNotFoundError: If public_id doesn't exist
        """
        try:
            resource = cloudinary.api.resource(identifier)
            
            metadata = {
                'bytes': resource.get('bytes', 0),
                'format': resource.get('format', 'unknown'),
                'width': resource.get('width', 0),
                'height': resource.get('height', 0),
                'created_at': resource.get('created_at', ''),
                'public_id': resource.get('public_id', identifier),
                'version': resource.get('version', 0),
                'resource_type': resource.get('resource_type', 'image'),
                'secure_url': resource.get('secure_url', ''),
                'folder': resource.get('folder', ''),
                'tags': resource.get('tags', [])
            }
            
            # Add any additional metadata that Cloudinary provides
            if 'etag' in resource:
                metadata['etag'] = resource['etag']
            
            if 'signature' in resource:
                metadata['signature'] = resource['signature']
            
            return metadata
            
        except CloudinaryError as e:
            if 'Not Found' in str(e) or 'Resource not found' in str(e):
                raise StorageNotFoundError(f"Cloudinary resource not found: {identifier}")
            
            logger.error(
                "Failed to get Cloudinary metadata",
                extra={
                    'public_id': identifier,
                    'error': str(e)
                }
            )
            raise StorageNotFoundError(f"Failed to get metadata for {identifier}: {str(e)}")
    
    def upload_video(self, file: BinaryIO, path: str, **kwargs) -> str:
        """
        Upload a video file to Cloudinary with automatic processing.
        
        This method uploads a video to Cloudinary with automatic transcoding
        to multiple formats and adaptive bitrate streaming (HLS). It includes
        retry logic for transient errors and generates unique public_ids.
        
        Args:
            file: Binary video file object to upload
            path: Logical path for organization (e.g., "reviews/service")
            **kwargs: Additional Cloudinary options (transformations, tags, etc.)
            
        Returns:
            str: Cloudinary public_id that can be used with other video methods
            
        Raises:
            StorageUploadError: If upload fails after retries
            CloudinaryAuthError: If authentication fails
            CloudinaryRateLimitError: If rate limit is exceeded
            StorageQuotaError: If storage quota is exceeded
        """
        # Generate unique public_id for video
        timestamp = int(datetime.now().timestamp())
        unique_id = str(uuid.uuid4())[:8]
        
        # Construct folder path
        folder_path = f"{self.config.folder_prefix}/{path.strip('/')}"
        public_id = f"{folder_path}/video-{unique_id}-{timestamp}"
        
        # Configure eager transformations for video processing
        eager_transformations = kwargs.get('eager', [
            {"streaming_profile": "hd", "format": "m3u8"},  # HLS streaming
            {"format": "mp4", "quality": "auto"},           # Progressive MP4
        ])
        
        upload_options = {
            'public_id': public_id,
            'resource_type': 'video',
            'eager': eager_transformations,
            'eager_async': False,  # Process immediately for synchronous upload
            'overwrite': False,
            **{k: v for k, v in kwargs.items() if k not in ['eager', 'resource_type']}
        }
        
        logger.info(
            "Starting Cloudinary video upload",
            extra={
                'public_id': public_id,
                'folder_path': folder_path,
                'eager_transformations': len(eager_transformations)
            }
        )
        
        # Retry logic for transient errors
        max_retries = 3
        base_delay = 1  # seconds
        
        for attempt in range(max_retries):
            try:
                start_time = time.time()
                
                # Reset file pointer to beginning
                file.seek(0)
                
                # Upload to Cloudinary
                result = cloudinary.uploader.upload(file, **upload_options)
                
                duration = time.time() - start_time
                
                logger.info(
                    "Cloudinary video upload successful",
                    extra={
                        'public_id': result['public_id'],
                        'bytes': result.get('bytes', 0),
                        'format': result.get('format', 'unknown'),
                        'width': result.get('width', 0),
                        'height': result.get('height', 0),
                        'video_duration': result.get('duration', 0),
                        'duration_seconds': round(duration, 2),
                        'attempt': attempt + 1,
                        'url': result.get('secure_url', '')
                    }
                )
                
                return result['public_id']
                
            except CloudinaryError as e:
                error_message = str(e)
                
                # Handle specific Cloudinary errors
                if 'Invalid API Key or API Secret' in error_message:
                    raise CloudinaryAuthError(f"Cloudinary authentication failed: {error_message}")
                
                if 'Rate limit' in error_message or 'Too Many Requests' in error_message:
                    retry_after = 60
                    raise CloudinaryRateLimitError(error_message, retry_after=retry_after)
                
                if 'quota' in error_message.lower() or 'limit exceeded' in error_message.lower():
                    raise StorageQuotaError(f"Cloudinary quota exceeded: {error_message}")
                
                # Check if retryable
                retryable_errors = [
                    'timeout', 'connection', 'network', 'temporary', 'server error'
                ]
                is_retryable = any(keyword in error_message.lower() for keyword in retryable_errors)
                
                if not is_retryable or attempt == max_retries - 1:
                    logger.error(
                        "Cloudinary video upload failed permanently",
                        extra={
                            'public_id': public_id,
                            'error': error_message,
                            'attempt': attempt + 1,
                            'retryable': is_retryable
                        }
                    )
                    raise StorageUploadError(
                        f"Cloudinary video upload failed: {error_message}",
                        retryable=is_retryable
                    )
                
                # Wait before retry with exponential backoff
                delay = base_delay * (2 ** attempt)
                logger.warning(
                    "Cloudinary video upload failed, retrying",
                    extra={
                        'public_id': public_id,
                        'error': error_message,
                        'attempt': attempt + 1,
                        'retry_delay_seconds': delay
                    }
                )
                time.sleep(delay)
            
            except Exception as e:
                logger.error(
                    "Unexpected error during Cloudinary video upload",
                    extra={
                        'public_id': public_id,
                        'error': str(e),
                        'attempt': attempt + 1
                    }
                )
                raise StorageUploadError(
                    f"Unexpected error during video upload: {str(e)}",
                    retryable=True
                )
        
        raise StorageUploadError("Video upload failed after all retry attempts", retryable=False)
    
    def get_video_url(self, identifier: str, format: str = 'mp4', **kwargs) -> str:
        """
        Generate a progressive video URL for direct playback.
        
        Args:
            identifier: Cloudinary public_id returned by upload_video()
            format: Video format (default: 'mp4')
            **kwargs: Additional transformation options
            
        Returns:
            str: Full HTTPS CDN URL to access the video
            
        Raises:
            StorageNotFoundError: If public_id doesn't exist
        """
        try:
            # Generate URL with video transformations
            url, _ = cloudinary.utils.cloudinary_url(
                identifier,
                resource_type='video',
                secure=True,
                format=format,
                quality='auto',
                fetch_format='auto',
                **kwargs
            )
            
            return url
            
        except CloudinaryError as e:
            raise StorageNotFoundError(f"Failed to generate video URL for {identifier}: {str(e)}")
    
    def get_streaming_url(self, identifier: str, **kwargs) -> str:
        """
        Generate an HLS streaming URL for adaptive bitrate playback.
        
        Args:
            identifier: Cloudinary public_id returned by upload_video()
            **kwargs: Additional transformation options
            
        Returns:
            str: Full HTTPS CDN URL for HLS streaming (m3u8 manifest)
            
        Raises:
            StorageNotFoundError: If public_id doesn't exist
        """
        try:
            # Generate HLS streaming URL
            url, _ = cloudinary.utils.cloudinary_url(
                identifier,
                resource_type='video',
                secure=True,
                format='m3u8',
                streaming_profile='hd',
                **kwargs
            )
            
            return url
            
        except CloudinaryError as e:
            raise StorageNotFoundError(f"Failed to generate streaming URL for {identifier}: {str(e)}")
    
    def get_thumbnail_url(self, identifier: str, time_offset: str = '2s', **kwargs) -> str:
        """
        Generate a video thumbnail URL at specified time offset.
        
        Args:
            identifier: Cloudinary public_id returned by upload_video()
            time_offset: Time offset for thumbnail (default: '2s')
            **kwargs: Additional transformation options
            
        Returns:
            str: Full HTTPS CDN URL to access the video thumbnail
            
        Raises:
            StorageNotFoundError: If public_id doesn't exist
        """
        try:
            # Generate thumbnail URL from video
            url, _ = cloudinary.utils.cloudinary_url(
                identifier,
                resource_type='video',
                secure=True,
                format='jpg',
                start_offset=time_offset,
                quality='auto',
                fetch_format='auto',
                **kwargs
            )
            
            return url
            
        except CloudinaryError as e:
            raise StorageNotFoundError(f"Failed to generate thumbnail URL for {identifier}: {str(e)}")
    
    def get_video_metadata(self, identifier: str) -> Dict[str, Any]:
        """
        Retrieve metadata for a stored video.
        
        This method returns comprehensive metadata from Cloudinary including
        duration, dimensions, format, file size, and other video properties.
        
        Args:
            identifier: Cloudinary public_id returned by upload_video()
            
        Returns:
            dict: Metadata dictionary with video information including:
                - duration: Video duration in seconds
                - width: Video width in pixels
                - height: Video height in pixels
                - format: Video format (mp4, mov, webm, etc.)
                - bytes: File size in bytes
                - created_at: Upload timestamp
                
        Raises:
            StorageNotFoundError: If public_id doesn't exist
        """
        try:
            resource = cloudinary.api.resource(identifier, resource_type='video')
            
            metadata = {
                'duration': resource.get('duration', 0),
                'width': resource.get('width', 0),
                'height': resource.get('height', 0),
                'format': resource.get('format', 'unknown'),
                'bytes': resource.get('bytes', 0),
                'created_at': resource.get('created_at', ''),
                'public_id': resource.get('public_id', identifier),
                'version': resource.get('version', 0),
                'resource_type': resource.get('resource_type', 'video'),
                'secure_url': resource.get('secure_url', ''),
                'folder': resource.get('folder', ''),
                'tags': resource.get('tags', [])
            }
            
            # Add video-specific metadata
            if 'bit_rate' in resource:
                metadata['bit_rate'] = resource['bit_rate']
            
            if 'frame_rate' in resource:
                metadata['frame_rate'] = resource['frame_rate']
            
            if 'audio' in resource:
                metadata['audio'] = resource['audio']
            
            return metadata
            
        except CloudinaryError as e:
            if 'Not Found' in str(e) or 'Resource not found' in str(e):
                raise StorageNotFoundError(f"Cloudinary video resource not found: {identifier}")
            
            logger.error(
                "Failed to get Cloudinary video metadata",
                extra={
                    'public_id': identifier,
                    'error': str(e)
                }
            )
            raise StorageNotFoundError(f"Failed to get video metadata for {identifier}: {str(e)}")