<?php

/**
 * JCOGS Image Pro - Face Detection Service
 * Native EE7 implementation with HAARPHP integration
 * 
 * @category   ExpressionEngine Add-on
 * @package    JCOGS Image Pro
 * @author     JCOGS Design <contact@jcogs.net>
 * @copyright  Copyright (c) 2021 - 2025 JCOGS Design
 * @license    https://jcogs.net/add-ons/license/jcogs_img_pro
 * @version    2.0.6
 * @link       https://JCOGS.net/
 * @since      2.0.0
 */

namespace JCOGSDesign\JCOGSImagePro\Service;

use JCOGSDesign\JCOGSImagePro\Library\HaarDetector;
use JCOGSDesign\JCOGSImagePro\Service\ServiceCache;
use JCOGSDesign\JCOGSImagePro\Service\Provider\FaceRecognitionProviderInterface;

/**
 * Face Detection Service
 * 
 * Provides face detection capabilities for both crop and filter operations.
 * Uses HAARPHP library with fallback to skin tone detection.
 * 
 * Provider Architecture Support:
 * Automatically uses the best available face recognition provider from the
 * ProviderRegistry. Falls back to built-in HAARPHP if no providers registered
 * or if provider detection fails.
 */
class FaceDetectionService
{
    /**
     * Quality preset configurations
     * 
     * Defines detection parameters for each quality mode:
     * - cascade: Cascade file to use
     * - baseScale: Initial detection scale
     * - scale_inc: Scale increment per pass
     * - increment: Step size in pixels
     * - min_neighbors: Minimum neighbor detections required
     * - doCannyPruning: Enable Canny edge pruning for speed
     */
    private const QUALITY_PRESETS = [
        'fast' => [
            'cascade' => 'haarcascade_frontalface_alt', // Using alt cascade (alt2 conversion failed)
            'baseScale' => 1.25,
            'scale_inc' => 1.25,
            'increment' => 0.15,
            'min_neighbors' => 1,
            'doCannyPruning' => true, // 2-3x speedup
        ],
        'balanced' => [
            'cascade' => 'haarcascade_frontalface_alt', // 22 stages (current)
            'baseScale' => 1.25,
            'scale_inc' => 1.1,
            'increment' => 0.1,
            'min_neighbors' => 1,
            'doCannyPruning' => false,
        ],
        'accurate' => [
            'cascade' => 'haarcascade_frontalface_alt', // Using alt cascade (default conversion failed)
            'baseScale' => 1.1,
            'scale_inc' => 1.05,
            'increment' => 0.05,
            'min_neighbors' => 2,
            'doCannyPruning' => false,
        ],
    ];
    
    private Utilities $utilities_service;
    
    /**
     * Best available face recognition provider (optional)
     * @var FaceRecognitionProviderInterface|null
     */
    private ?FaceRecognitionProviderInterface $provider = null;
    
    /**
     * Cross-sensitivity face detection cache
     * Structure: [image_hash => ['face_regions' => array, 'max_sensitivity' => int]]
     */
    private static array $face_detection_cache = [];
    
    /**
     * Haar cascade data cache (multiple cascades supported)
     * Structure: [cascade_name => cascade_data_array]
     */
    private static array $haar_cascade_data = [];

    public function __construct()
    {
        $this->utilities_service = ServiceCache::utilities();
        
        // Attempt to get best available provider from registry
        try {
            if (ee()->has('jcogs_img_pro:ProviderRegistry')) {
                $registry = ee('jcogs_img_pro:ProviderRegistry');
                $this->provider = $registry->getFaceRecognitionProvider();
                
                if ($this->provider !== null) {
                    $this->utilities_service->debug_message(
                        'Using face recognition provider: ' . $this->provider->getName() .
                        ' (priority: ' . $this->provider->getPriority() . ')'
                    );
                }
            }
        } catch (\Exception $e) {
            // Registry not available or error - use built-in implementation
            $this->utilities_service->debug_message(
                'Provider registry unavailable, using built-in HAAR detection: ' . $e->getMessage()
            );
        }
    }
    
    /**
     * Detect faces in image and return face regions
     * 
     * Uses cross-sensitivity caching: runs detection once at highest sensitivity
     * and filters results for lower sensitivities to avoid redundant HAAR runs.
     * 
     * Provider Architecture Support:
     * If a higher-priority provider is available, uses it. Otherwise falls back
     * to built-in HAARPHP detection.
     *
     * @param resource|\GdImage $image The source image (GD resource)
     * @param int|string $sensitivity Detection sensitivity (1-9, legacy compatible)
     * @param string $quality Quality mode: 'fast', 'balanced', or 'accurate'
     * @return array Array of detected face regions with bounding boxes
     */
    public function detect_faces($image, $sensitivity = 3, string $quality = 'balanced'): array
    {
        // Cast sensitivity to int for type safety
        $sensitivity = (int)$sensitivity;
        
        // Validate quality parameter
        if (!isset(self::QUALITY_PRESETS[$quality])) {
            $this->utilities_service->debug_message('Invalid quality mode: ' . $quality . ', using balanced');
            $quality = 'balanced';
        }
        
        $this->utilities_service->debug_message('Starting face detection - sensitivity: ' . $sensitivity . ', quality: ' . $quality);
        
        if ($image === null || (!is_resource($image) && !($image instanceof \GdImage))) {
            $this->utilities_service->debug_message('Invalid image resource provided');
            return [];
        }
        
        // If provider available, use it
        if ($this->provider !== null && $this->provider->getName() !== 'core_haar') {
            try {
                $this->utilities_service->debug_message('Using provider: ' . $this->provider->getName());
                
                $provider_results = $this->provider->detectFaces($image, [
                    'sensitivity' => $sensitivity,
                    'quality' => $quality
                ]);
                
                $this->utilities_service->debug_message('Provider found: ' . count($provider_results) . ' faces');
                
                // Convert provider format to internal format if needed
                return $this->_normalize_provider_results($provider_results);
                
            } catch (\Exception $e) {
                // Provider failed - log and fall back
                $this->utilities_service->debug_message(
                    'Provider detection failed: ' . $e->getMessage() . ', falling back to built-in'
                );
            }
        }
        
        // Use built-in HAAR implementation
        return $this->_builtin_haar_detection($image, $sensitivity, $quality);
    }
    
    /**
     * Built-in HAAR detection implementation
     * 
     * This is the original detection logic, preserved as fallback.
     * 
     * @param resource|\GdImage $image Image resource
     * @param int $sensitivity Detection sensitivity
     * @param string $quality Quality mode
     * @return array Detected face regions
     */
    private function _builtin_haar_detection($image, int $sensitivity, string $quality): array
    {
        $this->utilities_service->debug_message('Using built-in HAAR detection');
        
        // Generate cache key from image data
        $cache_key = $this->_generate_image_cache_key($image);
        
        // Check if we have cached results
        if (isset(self::$face_detection_cache[$cache_key])) {
            $cached_data = self::$face_detection_cache[$cache_key];
            
            // If cached sensitivity >= requested sensitivity, we can filter the results
            if ($cached_data['max_sensitivity'] >= $sensitivity) {
                $this->utilities_service->debug_message('using_cached_face_detection_results', [$cached_data['max_sensitivity'], $sensitivity]);
                
                // Filter face regions based on requested sensitivity
                $filtered_regions = $this->_filter_face_regions_by_sensitivity(
                    $cached_data['face_regions'], 
                    $cached_data['max_sensitivity'], 
                    $sensitivity
                );
                
                $this->utilities_service->debug_message('Filtered cached results: ' . count($filtered_regions) . ' faces');
                return $filtered_regions;
            }
        }
        
        // No suitable cached results - run detection at maximum sensitivity to cache for future use
        $detection_sensitivity = max($sensitivity, 7); // Use high sensitivity for comprehensive caching
        
        $this->utilities_service->debug_message('running_face_detection_sensitivity', [$detection_sensitivity, $sensitivity]);

        // Try HAAR cascade detection first
        $face_regions = $this->_detect_faces_haar($image, $detection_sensitivity, $quality);
        
        $this->utilities_service->debug_message('HAAR detection found: ' . count($face_regions) . ' faces');
        
        // If HAAR fails or finds no faces, try fallback detection
        if (empty($face_regions)) {
            $this->utilities_service->debug_message('HAAR failed, trying fallback detection');
            $face_regions = $this->_detect_faces_fallback($image);
            
            $this->utilities_service->debug_message('Fallback detection found: ' . count($face_regions) . ' faces');
        }
        
        // Cache the results with the actual detection sensitivity used
        self::$face_detection_cache[$cache_key] = [
            'face_regions' => $face_regions,
            'max_sensitivity' => $detection_sensitivity
        ];
        
        // Filter results to match requested sensitivity
        $filtered_regions = $this->_filter_face_regions_by_sensitivity(
            $face_regions, 
            $detection_sensitivity, 
            $sensitivity
        );
        
        $this->utilities_service->debug_message('Detection complete, returning ' . count($filtered_regions) . ' faces for sensitivity ' . $sensitivity);
        
        return $filtered_regions;
    }
    
    /**
     * Get bounding box encompassing all detected faces with optional margin
     *
     * @param array $face_regions Array of face regions from detect_faces()
     * @param int $margin Margin to add around bounding box in pixels
     * @return array|null Bounding box coordinates or null if no faces
     */
    public function get_bounding_box(array $face_regions, int $margin = 0): ?array
    {
        if (empty($face_regions)) {
            return null;
        }
        
        // Find overall bounding box of all faces
        $min_x = PHP_INT_MAX;
        $min_y = PHP_INT_MAX;
        $max_x = PHP_INT_MIN;
        $max_y = PHP_INT_MIN;
        
        foreach ($face_regions as $region) {
            $min_x = min($min_x, $region['min_x']);
            $min_y = min($min_y, $region['min_y']);
            $max_x = max($max_x, $region['max_x']);
            $max_y = max($max_y, $region['max_y']);
        }
        
        // Apply margin
        $min_x = max(0, $min_x - $margin);
        $min_y = max(0, $min_y - $margin);
        $max_x = $max_x + $margin;
        $max_y = $max_y + $margin;
        
        return [
            'min_x' => $min_x,
            'min_y' => $min_y,
            'max_x' => $max_x,
            'max_y' => $max_y,
            'width' => $max_x - $min_x,
            'height' => $max_y - $min_y
        ];
    }
    
    /**
     * Normalize provider results to internal format
     * 
     * Providers return: ['x', 'y', 'width', 'height', 'confidence', 'metadata']
     * Internal format: ['min_x', 'min_y', 'max_x', 'max_y', 'width', 'height']
     * 
     * @param array $provider_results Results from provider
     * @return array Normalized results
     */
    private function _normalize_provider_results(array $provider_results): array
    {
        $normalized = [];
        
        foreach ($provider_results as $face) {
            // If already in internal format, pass through
            if (isset($face['min_x']) && isset($face['max_x'])) {
                $normalized[] = $face;
                continue;
            }
            
            // Convert from provider format
            $normalized[] = [
                'min_x' => $face['x'],
                'min_y' => $face['y'],
                'max_x' => $face['x'] + $face['width'],
                'max_y' => $face['y'] + $face['height'],
                'width' => $face['width'],
                'height' => $face['height'],
                'confidence' => $face['confidence'] ?? 0.8,
                'metadata' => $face['metadata'] ?? [],
            ];
        }
        
        return $normalized;
    }
    
    /**
     * Get centroid of all detected faces
     *
     * @param array $face_regions Array of face regions from detect_faces()
     * @return array|null Centroid coordinates or null if no faces
     */
    public function get_centroid(array $face_regions): ?array
    {
        if (empty($face_regions)) {
            return null;
        }
        
        $total_x = 0;
        $total_y = 0;
        $count = 0;
        
        foreach ($face_regions as $region) {
            $center_x = ($region['min_x'] + $region['max_x']) / 2;
            $center_y = ($region['min_y'] + $region['max_y']) / 2;
            $total_x += $center_x;
            $total_y += $center_y;
            $count++;
        }
        
        return [
            'x' => intval($total_x / $count),
            'y' => intval($total_y / $count)
        ];
    }
    
    /**
     * Fallback face detection using basic skin tone analysis (from existing filter)
     *
     * @param resource $image The source image
     * @return array Array of detected face regions (format matches HAAR: min_x, min_y, max_x, max_y, width, height)
     */
    private function _detect_faces_fallback($image): array
    {
        $width = imagesx($image);
        $height = imagesy($image);
        $regions = [];
        
        // Downsample large images for performance
        // Fallback detection is computationally expensive on large images
        $max_fallback_size = 400; // Maximum dimension for fallback processing
        $should_downsample = $width > $max_fallback_size || $height > $max_fallback_size;
        
        if ($should_downsample) {
            // Calculate scale factor to fit within max size
            $scale = $max_fallback_size / max($width, $height);
            $scaled_width = (int)($width * $scale);
            $scaled_height = (int)($height * $scale);
            
            $this->utilities_service->debug_message(
                "Fallback: Downsampling {$width}×{$height} → {$scaled_width}×{$scaled_height} (scale: " . 
                number_format($scale, 3) . ")"
            );
            
            // Create downsampled image
            $scaled_image = imagecreatetruecolor($scaled_width, $scaled_height);
            imagecopyresampled(
                $scaled_image, $image,
                0, 0, 0, 0,
                $scaled_width, $scaled_height,
                $width, $height
            );
            
            // Run detection on downsampled image
            $skin_regions = $this->_detect_skin_tones($scaled_image);
            $face_candidates = $this->_group_skin_regions($skin_regions, $scaled_width, $scaled_height);
            
            $this->utilities_service->debug_message(
                "Fallback: Found " . count($face_candidates) . " skin region clusters on downsampled image"
            );
            
            // Scale coordinates back to original size
            foreach ($face_candidates as $candidate) {
                if ($this->_is_potential_face($candidate)) {
                    // Calculate dimensions from min/max coordinates
                    $region_width = $candidate['max_x'] - $candidate['min_x'];
                    $region_height = $candidate['max_y'] - $candidate['min_y'];
                    
                    // Scale back to original image coordinates
                    $regions[] = [
                        'min_x' => (int)($candidate['min_x'] / $scale),
                        'min_y' => (int)($candidate['min_y'] / $scale),
                        'max_x' => (int)($candidate['max_x'] / $scale),
                        'max_y' => (int)($candidate['max_y'] / $scale),
                        'width' => (int)($region_width / $scale),
                        'height' => (int)($region_height / $scale),
                    ];
                }
            }
            
            imagedestroy($scaled_image);
        } else {
            // Original path for small images
            $this->utilities_service->debug_message(
                "Fallback: Processing {$width}×{$height} without downsampling (under {$max_fallback_size}px threshold)"
            );
            
            $skin_regions = $this->_detect_skin_tones($image);
            $face_candidates = $this->_group_skin_regions($skin_regions, $width, $height);
            
            $this->utilities_service->debug_message(
                "Fallback: Found " . count($face_candidates) . " skin region clusters"
            );
            
            foreach ($face_candidates as $candidate) {
                if ($this->_is_potential_face($candidate)) {
                    // Calculate dimensions from min/max coordinates
                    $region_width = $candidate['max_x'] - $candidate['min_x'];
                    $region_height = $candidate['max_y'] - $candidate['min_y'];
                    
                    $regions[] = [
                        'min_x' => $candidate['min_x'],
                        'min_y' => $candidate['min_y'],
                        'max_x' => $candidate['max_x'],
                        'max_y' => $candidate['max_y'],
                        'width' => $region_width,
                        'height' => $region_height,
                    ];
                }
            }
        }
        
        $this->utilities_service->debug_message(
            "Fallback: Returning " . count($regions) . " potential face regions"
        );
        
        return $regions;
    }
    
    /**
     * Detect faces using HAARPHP library with quality presets
     *
     * @param resource|\GdImage $image The source image
     * @param int $sensitivity Sensitivity level (1-9, legacy compatible)
     * @param string $quality Quality mode ('fast', 'balanced', 'accurate')
     * @return array Array of detected face regions
     */
    private function _detect_faces_haar($image, int $sensitivity, string $quality = 'balanced'): array
    {
        try {
            // Get quality preset configuration
            $preset = self::QUALITY_PRESETS[$quality];
            
            // Load appropriate cascade for this quality mode
            $cascade_name = $preset['cascade'];
            $cascade_data = $this->_load_cascade($cascade_name);
            
            if ($cascade_data === null) {
                throw new \Exception('Failed to load cascade: ' . $cascade_name);
            }
            
            // Convert legacy sensitivity (1-9) to scale factor
            // Use exact Legacy formula for performance compatibility
            $sensitivity = min(max(1, $sensitivity), 9); // Normalise value to range 1-9
            $sensitivity = ($sensitivity - 3); // 3 == 0
            $scale = (5 + $sensitivity) / 15; // Normalise based on $sensitivity 1 => 20%, 9 => 93%.
            
            // Create face detector with selected cascade
            $face_detector = new HaarDetector($cascade_data);
            
            // Detect faces with quality-based parameters
            $found = $face_detector
                ->image($image, $scale)
                ->cannyThreshold(['low' => 80, 'high' => 200])
                ->detect(
                    $preset['baseScale'],
                    $preset['scale_inc'],
                    $preset['increment'],
                    $preset['min_neighbors'],
                    0.2,  // epsilon (overlap threshold)
                    $preset['doCannyPruning']
                );
            
            $face_regions = [];
            
            if ($found && $face_detector->objects) {
                foreach ($face_detector->objects as $face) {
                    $face_regions[] = [
                        'min_x' => $face->x,
                        'min_y' => $face->y,
                        'max_x' => $face->x + $face->width,
                        'max_y' => $face->y + $face->height,
                        'width' => $face->width,
                        'height' => $face->height
                    ];
                }
            }
            
            return $face_regions;
            
        } catch (\Exception $e) {
            $this->utilities_service->debug_message('HAAR detection error: ' . $e->getMessage());
            // Fall back to simple skin tone detection if HAAR fails
            return [];
        }
    }
    
    /**
     * Load Haar cascade data file
     * 
     * Implements multi-cascade caching to support different quality modes.
     *
     * @param string $cascade_name Cascade filename without .php extension
     * @return array|null Cascade data array or null on failure
     */
    private function _load_cascade(string $cascade_name): ?array
    {
        // Check if cascade already loaded
        if (isset(self::$haar_cascade_data[$cascade_name])) {
            $this->utilities_service->debug_message('Using cached cascade: ' . $cascade_name);
            return self::$haar_cascade_data[$cascade_name];
        }
        
        // Load cascade file
        $cascade_file = __DIR__ . '/../Library/' . $cascade_name . '.php';
        
        if (!file_exists($cascade_file)) {
            $this->utilities_service->debug_message('Cascade file not found: ' . $cascade_file);
            return null;
        }
        
        // Load cascade data
        require $cascade_file;
        
        // Extract variable name from cascade filename (e.g., haarcascade_frontalface_alt -> $haarcascade_frontalface_alt)
        $var_name = $cascade_name;
        
        if (!isset($$var_name) || !is_array($$var_name)) {
            $this->utilities_service->debug_message('Invalid cascade data in: ' . $cascade_file);
            return null;
        }
        
        // Cache for reuse
        self::$haar_cascade_data[$cascade_name] = $$var_name;
        $this->utilities_service->debug_message('Loaded and cached cascade: ' . $cascade_name);
        
        return $$var_name;
    }
    
    /**
     * Detect skin tone regions in the image
     *
     * @param resource $image Source image
     * @return array Array of skin tone pixels
     */
    private function _detect_skin_tones($image): array
    {
        $width = imagesx($image);
        $height = imagesy($image);
        $skin_pixels = [];
        
        for ($x = 0; $x < $width; $x += 2) { // Sample every other pixel for performance
            for ($y = 0; $y < $height; $y += 2) {
                $color = imagecolorat($image, $x, $y);
                $r = ($color >> 16) & 0xFF;
                $g = ($color >> 8) & 0xFF;
                $b = $color & 0xFF;
                
                if ($this->_is_skin_tone($r, $g, $b)) {
                    $skin_pixels[] = ['x' => $x, 'y' => $y];
                }
            }
        }
        
        return $skin_pixels;
    }
    
    /**
     * Group skin pixels into potential face regions
     *
     * @param array $skin_pixels Array of skin tone pixels
     * @param int $width Image width
     * @param int $height Image height
     * @return array Array of face candidate regions
     */
    private function _group_skin_regions(array $skin_pixels, int $width, int $height): array
    {
        if (empty($skin_pixels)) {
            return [];
        }
        
        // Simple clustering by proximity
        $clusters = [];
        $cluster_distance = 30; // Maximum distance to be in same cluster
        
        foreach ($skin_pixels as $pixel) {
            $added_to_cluster = false;
            
            foreach ($clusters as &$cluster) {
                // Check if pixel is close to any pixel in this cluster
                foreach ($cluster['pixels'] as $cluster_pixel) {
                    $distance = sqrt(
                        pow($pixel['x'] - $cluster_pixel['x'], 2) +
                        pow($pixel['y'] - $cluster_pixel['y'], 2)
                    );
                    
                    if ($distance <= $cluster_distance) {
                        $cluster['pixels'][] = $pixel;
                        $this->_update_cluster_bounds($cluster, $pixel);
                        $added_to_cluster = true;
                        break 2;
                    }
                }
            }
            
            if (!$added_to_cluster) {
                // Create new cluster
                $clusters[] = [
                    'pixels' => [$pixel],
                    'min_x' => $pixel['x'],
                    'max_x' => $pixel['x'],
                    'min_y' => $pixel['y'],
                    'max_y' => $pixel['y']
                ];
            }
        }
        
        return $clusters;
    }
    
    /**
     * Check if a cluster could be a face
     *
     * @param array $cluster Cluster to evaluate
     * @return bool True if potential face
     */
    private function _is_potential_face(array $cluster): bool
    {
        $width = $cluster['max_x'] - $cluster['min_x'];
        $height = $cluster['max_y'] - $cluster['min_y'];
        
        // Filter by size (faces should be reasonably sized)
        if ($width < 20 || $height < 20) return false;
        if ($width > 200 || $height > 200) return false;
        
        // Filter by aspect ratio (faces are roughly oval)
        $ratio = $height / $width;
        if ($ratio < 0.8 || $ratio > 1.5) return false;
        
        // Filter by pixel density (should have enough skin pixels)
        $area = $width * $height;
        $pixel_density = count($cluster['pixels']) / $area;
        if ($pixel_density < 0.1) return false;
        
        return true;
    }
    
    /**
     * Check if RGB values represent a skin tone
     *
     * @param int $r Red value
     * @param int $g Green value
     * @param int $b Blue value
     * @return bool True if likely skin tone
     */
    private function _is_skin_tone(int $r, int $g, int $b): bool
    {
        // Simple skin tone detection based on common ranges
        // This is a basic approach - real implementations use HSV or YCbCr
        
        // Rule 1: R > G > B generally for skin tones
        if ($r <= $g || $g <= $b) {
            return false;
        }
        
        // Rule 2: Check ranges for typical skin tones
        if ($r < 95 || $r > 255) return false;
        if ($g < 40 || $g > 220) return false;
        if ($b < 20 || $b > 170) return false;
        
        // Rule 3: Check ratios
        if (($r - $g) < 15) return false;
        if (($g - $b) < 5) return false;
        
        return true;
    }
    
    /**
     * Update cluster boundaries with new pixel
     *
     * @param array &$cluster Cluster to update
     * @param array $pixel New pixel coordinates
     */
    private function _update_cluster_bounds(array &$cluster, array $pixel): void
    {
        $cluster['min_x'] = min($cluster['min_x'], $pixel['x']);
        $cluster['max_x'] = max($cluster['max_x'], $pixel['x']);
        $cluster['min_y'] = min($cluster['min_y'], $pixel['y']);
        $cluster['max_y'] = max($cluster['max_y'], $pixel['y']);
    }
    
    /**
     * Generate cache key from image data for cross-sensitivity caching
     *
     * @param resource $image GD resource
     * @return string Cache key based on image content
     */
    private function _generate_image_cache_key($image): string
    {
        // Get image dimensions for key
        $width = imagesx($image);
        $height = imagesy($image);
        
        // Create a lightweight hash from image data
        // Sample a few pixels for performance (corners + center)
        $sample_points = [
            [0, 0], [$width-1, 0], [0, $height-1], [$width-1, $height-1], // corners
            [intval($width/2), intval($height/2)] // center
        ];
        
        $sample_data = '';
        foreach ($sample_points as [$x, $y]) {
            $color = imagecolorat($image, $x, $y);
            $sample_data .= dechex($color);
        }
        
        // Include dimensions and sample data in hash
        return 'face_cache_' . md5($width . 'x' . $height . '_' . $sample_data);
    }
    
    /**
     * Filter face regions based on sensitivity difference
     * 
     * Lower sensitivity should show fewer/more confident faces.
     * This simulates running detection at lower sensitivity by filtering results.
     *
     * @param array $face_regions Face regions from high-sensitivity detection
     * @param int $cached_sensitivity The sensitivity used for cached detection
     * @param int $requested_sensitivity The requested sensitivity level
     * @return array Filtered face regions
     */
    private function _filter_face_regions_by_sensitivity(array $face_regions, int $cached_sensitivity, int $requested_sensitivity): array
    {
        // If requested sensitivity is higher or equal, return all faces
        if ($requested_sensitivity >= $cached_sensitivity) {
            return $face_regions;
        }
        
        // Lower sensitivity = more conservative = fewer faces
        // Filter based on face size (larger faces = more confident detection)
        $sensitivity_factor = $requested_sensitivity / $cached_sensitivity;
        
        $filtered_regions = [];
        foreach ($face_regions as $region) {
            $face_area = $region['width'] * $region['height'];
            
            // Keep larger faces for lower sensitivity (more conservative)
            // Minimum area threshold increases as sensitivity decreases
            $min_area_threshold = 2000 * (1 - $sensitivity_factor); // Adjust multiplier as needed
            
            if ($face_area >= $min_area_threshold) {
                $filtered_regions[] = $region;
            }
        }
        
        $this->utilities_service->debug_message(
            "Sensitivity filtering: {$cached_sensitivity} → {$requested_sensitivity}, " . 
            count($face_regions) . " → " . count($filtered_regions) . " faces"
        );
        
        return $filtered_regions;
    }
}
