import os
import random
import logging
import time
import threading
from collections import deque
from pathlib import Path
from typing import Optional, Tuple, Literal, List
from PIL import Image
import io
from django.http import HttpResponse, HttpResponseNotFound
from django.views.decorators.http import require_http_methods
from django.core.cache import cache
from django.conf import settings
from concurrent.futures import ThreadPoolExecutor

# Configure logging
logger = logging.getLogger(__name__)

FitMode = Literal['clip', 'crop', 'fill', 'scale']

# Configuration constants
IMAGES_FOLDER = settings.IMAGES_FOLDER
CACHE_TIMEOUT = 600  # 10 minutes for HTTP caching
MAX_FILE_SIZE = 3 * 1024 * 1024  # 3MB in bytes
SUPPORTED_FORMATS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff'}
MAX_RANDOM_ATTEMPTS = 30  # Maximum number of attempts to find a valid random image
MINIMUM_IMAGE_SIZE = 300  # Minimum width and height for valid images
executor = ThreadPoolExecutor(max_workers=4)

# Queue to track recently served images (thread-safe)
recent_images_queue = deque(maxlen=50)
recent_images_lock = threading.Lock()

# Initialize random seed for true randomness
random.seed(time.time())

def is_valid_image_fast(file_path: Path) -> bool:
    """Fast check if the file is likely a valid image without opening it."""
    try:
        # Check file size first (fastest check)
        if file_path.stat().st_size > MAX_FILE_SIZE:
            return False

        # Check if file extension is supported
        if file_path.suffix.lower() not in SUPPORTED_FORMATS:
            return False

        # Quick file signature check (magic numbers) without full PIL verification
        with open(file_path, 'rb') as f:
            header = f.read(16)

        # Check common image file signatures
        if len(header) < 4:
            return False

        # Check if file contains valid image data
        signature_valid = False

        # JPEG signature
        if header[:2] == b'\xff\xd8':
            signature_valid = True
        # PNG signature
        elif header[:8] == b'\x89PNG\r\n\x1a\n':
            signature_valid = True
        # GIF signature
        elif header[:6] in (b'GIF87a', b'GIF89a'):
            signature_valid = True
        # WebP signature
        elif header[:4] == b'RIFF' and header[8:12] == b'WEBP':
            signature_valid = True
        # BMP signature
        elif header[:2] == b'BM':
            signature_valid = True
        # TIFF signatures
        elif header[:4] in (b'II*\x00', b'MM\x00*'):
            signature_valid = True

        if not signature_valid:
            return False

        # Check image dimensions to ensure both width and height are at least 512px
        # This requires opening the image, but it's necessary to check dimensions
        try:
            with Image.open(file_path) as img:
                width, height = img.size
                if width < MINIMUM_IMAGE_SIZE or height < MINIMUM_IMAGE_SIZE:
                    logger.debug(f"Image too small: {file_path.name} ({width}x{height})")
                    return False
        except Exception:
            return False

        return True

    except (OSError, IOError, Exception):
        return False

def is_valid_image_deep(file_path: Path) -> bool:
    """Deep validation using PIL (only called when actually serving the image)."""
    try:
        with Image.open(file_path) as img:
            img.verify()
        return True
    except (OSError, IOError, Exception):
        return False

def get_image_paths() -> List[Path]:
    """Get a list of valid image paths from the root folder only.

    Note: This function is only used for stats and is not part of the main API serving flow.
    """
    if not IMAGES_FOLDER or not os.path.exists(IMAGES_FOLDER):
        logger.warning(f"Images folder not found or not configured: {IMAGES_FOLDER}")
        return []

    logger.info(f"Scanning for images in root folder: {IMAGES_FOLDER}")

    # Collect all image files from the root folder only
    valid_images = []
    base_path = Path(IMAGES_FOLDER)

    # Get files from the root folder
    for item in base_path.iterdir():
        if item.is_file() and item.suffix.lower() in SUPPORTED_FORMATS:
            if is_valid_image_fast(item):
                valid_images.append(item)

    logger.info(f"Found {len(valid_images)} valid images in the root folder")
    return valid_images

# Cache invalidation function removed as we no longer use caching in the new approach

def get_image_stats() -> dict:
    """Get statistics about available images."""
    image_paths = get_image_paths()
    if not image_paths:
        return {'total': 0, 'folders': 0, 'total_size_mb': 0}

    folders = set()
    total_size = 0

    for img_path in image_paths:
        folders.add(str(img_path.parent.relative_to(Path(IMAGES_FOLDER))))
        total_size += img_path.stat().st_size

    return {
        'total': len(image_paths),
        'folders': len(folders),
        'total_size_mb': round(total_size / (1024 * 1024), 2),
        'avg_size_mb': round((total_size / len(image_paths)) / (1024 * 1024), 2) if image_paths else 0
    }

def resize_image(image: Image.Image, width: Optional[int], height: Optional[int], fit: FitMode = 'scale') -> Image.Image:
    """Resize image according to the specified fit mode.

    Args:
        image: Source image
        width: Target width
        height: Target height
        fit: Resize fit mode:
            - 'clip': Resize maintaining aspect ratio, clip excess parts
            - 'crop': Center crop to exact dimensions
            - 'fill': Stretch to exact dimensions
            - 'scale': Scale maintaining aspect ratio (default)
    """
    if not width and not height:
        return image

    orig_width, orig_height = image.size

    if fit == 'fill':
        # Stretch to exact dimensions
        if width and height:
            return image.resize((width, height), Image.Resampling.LANCZOS)

    elif fit == 'crop':
        # Center crop to exact dimensions
        if width and height:
            # First scale to cover target dimensions
            ratio = max(width / orig_width, height / orig_height)
            new_width = int(orig_width * ratio)
            new_height = int(orig_height * ratio)
            resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS)

            # Then crop to center
            left = (new_width - width) // 2
            top = (new_height - height) // 2
            right = left + width
            bottom = top + height
            return resized.crop((left, top, right, bottom))

    elif fit == 'clip':
        # Scale maintaining aspect ratio, then clip
        if width and height:
            ratio = min(width / orig_width, height / orig_height)
            new_width = int(orig_width * ratio)
            new_height = int(orig_height * ratio)
            return image.resize((new_width, new_height), Image.Resampling.LANCZOS)

    # Default: 'scale' - Maintain aspect ratio
    if width and height:
        ratio = min(width / orig_width, height / orig_height)
        new_width = int(orig_width * ratio)
        new_height = int(orig_height * ratio)
        return image.resize((new_width, new_height), Image.Resampling.LANCZOS)
    elif width:
        ratio = width / orig_width
        return image.resize((width, int(orig_height * ratio)), Image.Resampling.LANCZOS)
    else:
        ratio = height / orig_height
        return image.resize((int(orig_width * ratio), height), Image.Resampling.LANCZOS)

def get_supported_image_path() -> Optional[Path]:
    """Get a random valid image path from root folder only, avoiding recently served images."""
    if not IMAGES_FOLDER or not os.path.exists(IMAGES_FOLDER):
        logger.warning(f"Images folder not found or not configured: {IMAGES_FOLDER}")
        return None

    base_path = Path(IMAGES_FOLDER)
    attempts = 0

    # Get current list of recent images (thread-safe)
    with recent_images_lock:
        recent_images = set(recent_images_queue)

    while attempts < MAX_RANDOM_ATTEMPTS:
        attempts += 1

        try:
            # Get all image files from the root folder only (no subdirectories)
            files_in_dir = []
            for item in base_path.iterdir():
                if item.is_file() and item.suffix.lower() in SUPPORTED_FORMATS:
                    # Skip images that were recently served
                    if item.name not in recent_images:
                        files_in_dir.append(item)

            if not files_in_dir:
                # If no non-recent images available, clear the recent queue and try again
                if recent_images and attempts == 1:
                    logger.info("All available images were recently served, clearing recent images queue")
                    with recent_images_lock:
                        recent_images_queue.clear()
                        recent_images = set()
                    continue
                else:
                    logger.warning("No image files found in root folder")
                    return None

            # Ensure truly random selection
            random.seed(time.time() + hash(str(os.urandom(8))))

            # Randomly pick a file
            selected_file = random.SystemRandom().choice(files_in_dir)

            # Validate the file
            if selected_file.exists() and is_valid_image_fast(selected_file):
                file_size_mb = selected_file.stat().st_size / (1024 * 1024)

                # Add to recent images queue (thread-safe)
                with recent_images_lock:
                    recent_images_queue.append(selected_file.name)

                logger.debug(f"Selected random image: file='{selected_file.name}', "
                          f"size={file_size_mb:.2f}MB, attempt={attempts}/{MAX_RANDOM_ATTEMPTS}, "
                          f"recent_queue_size={len(recent_images_queue)}")

                return selected_file

            if attempts % 5 == 0:
                logger.debug(f"Still searching for valid image, attempt {attempts}/{MAX_RANDOM_ATTEMPTS}")

        except (OSError, IOError, Exception) as e:
            logger.warning(f"Error during file selection: {str(e)}, attempt {attempts}")

    logger.warning(f"Failed to find valid image after {MAX_RANDOM_ATTEMPTS} attempts")
    return None

def get_random_image() -> Optional[Path]:
    """Get a random valid image path from subfolders only."""
    return get_random_image_fast()

def process_image(image_path: Path, width: Optional[int] = None, height: Optional[int] = None,
                fit: FitMode = 'scale') -> Tuple[bytes, str]:
    """Process image with the specified dimensions and fit mode."""

    # Deep validation only when actually processing
    if not is_valid_image_deep(image_path):
        raise ValueError(f"Invalid image file: {image_path}")

    with Image.open(image_path) as img:
        # Convert RGBA to RGB if necessary
        if img.mode in ('RGBA', 'LA'):
            background = Image.new('RGB', img.size, (255, 255, 255))
            background.paste(img, mask=img.split()[-1])
            img = background
        elif img.mode != 'RGB':
            img = img.convert('RGB')

        if width or height:
            img = resize_image(img, width, height, fit)

        # Optimize output
        buffer = io.BytesIO()
        img.save(buffer, format='JPEG', quality=85, optimize=True)
        return buffer.getvalue(), 'image/jpeg'

# Performance optimization constants
CACHE_INDEX_TTL = 300  # 5 minutes cache for folder index
MAX_DIRECTORY_SCAN = 5000  # Stop scanning if directory has more than this many files

def get_random_image_fast() -> Optional[Path]:
    """Fast random image selection optimized for large collections - subfolder only approach."""
    if not IMAGES_FOLDER or not os.path.exists(IMAGES_FOLDER):
        logger.warning(f"Images folder not found or not configured: {IMAGES_FOLDER}")
        return None

    base_path = Path(IMAGES_FOLDER)

    # Always use hierarchical selection from subfolders only
    return get_random_image_hierarchical(base_path)

def get_random_image_hierarchical(base_path: Path) -> Optional[Path]:
    """Hierarchical random selection: pick random subfolder first, then random file.

    Only selects from subfolders, excludes root directory.
    Assumes flat structure within subfolders (no nested subdirectories).

    Robustness notes:
    - Candidates are validated during the scan (not after picking), so a folder
      whose first few files are invalid (tiny/corrupt/oversized) doesn't go
      permanently dark — directory iteration order is stable, so validating
      only the picked candidate used to fail the same way on every attempt.
    - If a full pass finds nothing because every valid candidate is in the
      recently-served queue, a second pass ignores the queue. Recency
      avoidance is best-effort and must never cause a hard 404: the queue only
      rotates on success, so without this fallback a run of failures froze the
      queue and the API degraded until process restart.
    """
    try:
        # Get only subdirectories (exclude root directory)
        directories = []
        for item in base_path.iterdir():
            if item.is_dir():
                directories.append(item)

        if not directories:
            logger.warning("No subfolders found in images directory")
            return None

        for ignore_recent in (False, True):
            # Try up to 20 random directories per pass
            for attempt in range(min(20, len(directories))):
                random_dir = random.choice(directories)

                # Quick check if directory has images (flat structure only)
                image_files = []
                file_count = 0

                for item in random_dir.iterdir():
                    # Skip subdirectories - only process files
                    if not item.is_file():
                        continue

                    file_count += 1
                    if file_count > MAX_DIRECTORY_SCAN:
                        logger.warning(f"Directory {random_dir.name} has too many files, skipping")
                        break

                    if item.suffix.lower() in SUPPORTED_FORMATS:
                        # Skip recently served images (unless we're on the
                        # fallback pass that ignores the recency queue)
                        if not ignore_recent:
                            with recent_images_lock:
                                if item.name in recent_images_queue:
                                    continue

                        # Keep only candidates that pass validation, so an
                        # unlucky pick can't waste the whole attempt
                        if is_valid_image_fast(item):
                            image_files.append(item)

                        # Early termination: if we found some images, we can select randomly
                        if len(image_files) >= 5:
                            break

                if image_files:
                    selected_file = random.choice(image_files)
                    # Add to recent images queue
                    with recent_images_lock:
                        recent_images_queue.append(selected_file.name)

                    logger.debug(f"Hierarchical selection: folder='{random_dir.name}', "
                               f"file='{selected_file.name}', candidates={len(image_files)}, "
                               f"ignore_recent={ignore_recent}")
                    return selected_file

            if ignore_recent:
                break
            with recent_images_lock:
                had_recent = len(recent_images_queue) > 0
            if not had_recent:
                break
            logger.info("First pass found no non-recent images; retrying without the recency filter")

        logger.warning("No valid images found in any subfolder after 20 attempts")
        return None

    except Exception as e:
        logger.error(f"Error in hierarchical selection: {e}")
        return None

def get_random_image_sampling(base_path: Path) -> Optional[Path]:
    """Random sampling approach: avoid full directory enumeration."""
    try:
        # Get a random sample of files without building complete list
        all_files = list(base_path.iterdir())

        # If too many files, use random sampling
        if len(all_files) > MAX_DIRECTORY_SCAN:
            # Random sampling approach
            sample_size = min(100, len(all_files))
            sampled_files = random.sample(all_files, sample_size)
        else:
            sampled_files = all_files

        # Filter and validate sampled files
        for item in sampled_files:
            if (item.is_file() and
                item.suffix.lower() in SUPPORTED_FORMATS):

                # Skip recently served images
                with recent_images_lock:
                    if item.name in recent_images_queue:
                        continue

                if is_valid_image_fast(item):
                    # Add to recent images queue
                    with recent_images_lock:
                        recent_images_queue.append(item.name)

                    logger.debug(f"Sampling selection: file='{item.name}', "
                               f"total_files={len(all_files)}, sampled={len(sampled_files)}")
                    return item

        logger.warning(f"No valid images found in sample of {len(sampled_files)} files")
        return None

    except Exception as e:
        logger.error(f"Error in sampling selection: {e}")
        return None

def get_cached_folder_stats() -> dict:
    """Get cached statistics about folder structure for performance monitoring."""
    cache_key = "image_folder_stats"
    stats = cache.get(cache_key)

    if stats is None:
        stats = analyze_folder_structure()
        cache.set(cache_key, stats, CACHE_INDEX_TTL)

    return stats

def analyze_folder_structure() -> dict:
    """Analyze the subfolder structure to provide performance insights."""
    if not IMAGES_FOLDER or not os.path.exists(IMAGES_FOLDER):
        return {'error': 'Images folder not found'}

    base_path = Path(IMAGES_FOLDER)
    stats = {
        'root_images': 0,  # Not used in subfolder-only approach
        'subfolders': [],
        'total_images': 0,
        'recommended_strategy': 'hierarchical'  # Always hierarchical for subfolder approach
    }

    try:
        # Only analyze subfolders (skip root directory images)
        for item in base_path.iterdir():
            if item.is_dir():
                folder_image_count = 0
                for subitem in item.iterdir():
                    # Only count files, skip nested subdirectories
                    if subitem.is_file() and subitem.suffix.lower() in SUPPORTED_FORMATS:
                        folder_image_count += 1
                        if folder_image_count > 1000:  # Stop counting after 1000
                            break

                if folder_image_count > 0:
                    stats['subfolders'].append({
                        'name': item.name,
                        'image_count': folder_image_count
                    })

        stats['total_images'] = sum(f['image_count'] for f in stats['subfolders'])

        # Always recommend hierarchical for subfolder-only approach
        if len(stats['subfolders']) > 0:
            stats['recommended_strategy'] = 'hierarchical'
        else:
            stats['recommended_strategy'] = 'none_found'

    except Exception as e:
        stats['error'] = str(e)

    return stats

@require_http_methods(["GET"])
def random_image(request, width: Optional[int] = None, height: Optional[int] = None):
    """API endpoint to serve random images with enhanced logging and validation."""
    if not IMAGES_FOLDER:
        logger.error("Images folder not configured - set IMAGES_FOLDER environment variable")
        return HttpResponseNotFound("Images folder not configured")

    if not os.path.exists(IMAGES_FOLDER):
        logger.error(f"Images folder does not exist: {IMAGES_FOLDER}")
        return HttpResponseNotFound("Images folder not found")

    # Convert string parameters to integers if they exist
    try:
        if width is not None:
            width = int(width)
        if height is not None:
            height = int(height)
    except (ValueError, TypeError):
        return HttpResponseNotFound("Width and height must be valid integers")

    # Get fit mode from query parameters, default to 'scale'
    fit = request.GET.get('fit', 'scale')
    if fit not in ('clip', 'crop', 'fill', 'scale'):
        logger.warning(f"Invalid fit mode requested: {fit}")
        return HttpResponseNotFound("Invalid fit mode. Supported modes: clip, crop, fill, scale")

    # Validate dimensions
    if width is not None and width < 300:
        return HttpResponseNotFound("Width must be 300px or larger")
    if height is not None and height < 300:
        return HttpResponseNotFound("Height must be 300px or larger")
    if width is not None and width > 4096:
        return HttpResponseNotFound("Width too large (max 4096px)")
    if height is not None and height > 4096:
        return HttpResponseNotFound("Height too large (max 4096px)")

    try:
        # Get a random valid image directly - the new get_random_image already has retry logic
        image_path = get_random_image()

        if not image_path:
            logger.error("Could not find a suitable image")
            return HttpResponseNotFound("No valid images available")

        # Log the request details
        client_ip = request.META.get('HTTP_X_FORWARDED_FOR',
                                   request.META.get('REMOTE_ADDR', 'unknown'))
        # Use thread pool for image processing
        start_time = time.time()
        image_data, content_type = executor.submit(
            process_image, image_path, width, height, fit
        ).result()
        processing_time = time.time() - start_time

        logger.info(f"Image processed successfully in {processing_time:.3f}s, "
                   f"output_size={len(image_data)/1024:.1f}KB")

        response = HttpResponse(image_data, content_type=content_type)
        response['Cache-Control'] = f'public, max-age={CACHE_TIMEOUT}'
        response['X-Image-Source'] = str(image_path.relative_to(Path(IMAGES_FOLDER)))
        return response

    except ValueError as e:
        logger.error(f"Image validation error: {str(e)}")
        return HttpResponseNotFound("Selected image is invalid")
    except Exception as e:
        logger.error(f"Error processing image {image_path if 'image_path' in locals() else 'unknown'}: {str(e)}")
        return HttpResponseNotFound(f"Error processing image: {str(e)}")
