#!/usr/bin/env python3
"""
Face detection, encoding, and matching functionality for PunimTag
"""

import os
import tempfile
import time
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from typing import List, Dict, Tuple, Optional
from functools import lru_cache

# DeepFace library for face detection and recognition
try:
    from deepface import DeepFace
    DEEPFACE_AVAILABLE = True
except ImportError:
    DEEPFACE_AVAILABLE = False
    print("⚠️  Warning: DeepFace not available, some features may not work")

from src.core.config import (
    DEFAULT_FACE_DETECTION_MODEL, 
    DEFAULT_FACE_TOLERANCE, 
    MIN_FACE_QUALITY,
    DEEPFACE_DETECTOR_BACKEND,
    DEEPFACE_MODEL_NAME,
    DEEPFACE_ENFORCE_DETECTION,
    DEEPFACE_ALIGN_FACES,
    MIN_FACE_CONFIDENCE,
    MIN_FACE_SIZE,
    MAX_FACE_SIZE,
    USE_CALIBRATED_CONFIDENCE,
    CONFIDENCE_CALIBRATION_METHOD
)
from src.core.database import DatabaseManager
from src.utils.exif_utils import EXIFOrientationHandler
from src.utils.pose_detection import PoseDetector, RETINAFACE_AVAILABLE


class FaceProcessor:
    """Handles face detection, encoding, and matching operations"""
    
    def __init__(self, db_manager: DatabaseManager, verbose: int = 0,
                 detector_backend: str = None, model_name: str = None):
        """Initialize face processor with DeepFace settings
        
        Args:
            db_manager: Database manager instance
            verbose: Verbosity level (0-3)
            detector_backend: DeepFace detector backend (retinaface, mtcnn, opencv, ssd)
                            If None, uses DEEPFACE_DETECTOR_BACKEND from config
            model_name: DeepFace model name (ArcFace, Facenet, Facenet512, VGG-Face)
                       If None, uses DEEPFACE_MODEL_NAME from config
        """
        self.db = db_manager
        self.verbose = verbose
        self.detector_backend = detector_backend or DEEPFACE_DETECTOR_BACKEND
        self.model_name = model_name or DEEPFACE_MODEL_NAME
        self._face_encoding_cache = {}
        self._image_cache = {}
        
        # Initialize pose detector with graceful fallback
        self.pose_detector = None
        if RETINAFACE_AVAILABLE:
            try:
                self.pose_detector = PoseDetector()
                if self.verbose >= 2:
                    print(f"   Pose detection: enabled")
            except Exception as e:
                if self.verbose >= 1:
                    print(f"⚠️  Pose detection not available: {e}")
                self.pose_detector = None
        else:
            if self.verbose >= 2:
                print(f"   Pose detection: RetinaFace not available")
        
        if self.verbose >= 2:
            print(f"🔧 FaceProcessor initialized:")
            print(f"   Detector: {self.detector_backend}")
            print(f"   Model: {self.model_name}")
    
    @lru_cache(maxsize=1000)
    def _get_cached_face_encoding(self, face_id: int, encoding_bytes: bytes) -> np.ndarray:
        """Cache face encodings to avoid repeated numpy conversions"""
        return np.frombuffer(encoding_bytes, dtype=np.float64)
    
    def _clear_caches(self):
        """Clear all caches to free memory"""
        self._face_encoding_cache.clear()
        self._image_cache.clear()
        self._get_cached_face_encoding.cache_clear()
    
    def cleanup_face_crops(self, current_face_crop_path=None):
        """Clean up face crop files and caches"""
        # Clean up current face crop if provided
        if current_face_crop_path and os.path.exists(current_face_crop_path):
            try:
                os.remove(current_face_crop_path)
            except:
                pass  # Ignore cleanup errors
        
        # Clean up all cached face crop files
        for cache_key, cached_path in list(self._image_cache.items()):
            if os.path.exists(cached_path):
                try:
                    os.remove(cached_path)
                except:
                    pass  # Ignore cleanup errors
        
        # Clear caches
        self._clear_caches()
    
    def process_faces(self, limit: Optional[int] = None, model: str = DEFAULT_FACE_DETECTION_MODEL, progress_callback=None, stop_event=None) -> int:
        """Process unprocessed photos for faces
        
        If provided, progress_callback will be called as progress_callback(index, total, filename)
        where index is 1-based count of processed photos so far.
        
        Args:
            limit: Maximum number of photos to process. If None, process all unprocessed photos.
        """
        # Start overall timing
        overall_start_time = time.time()
        
        unprocessed = self.db.get_unprocessed_photos(limit)
        
        if not unprocessed:
            print("✅ No unprocessed photos found")
            return 0
        
        print(f"🔍 Processing {len(unprocessed)} photos for faces...")
        processed_count = 0
        
        # Timing tracking variables
        total_face_detection_time = 0.0
        total_faces_found = 0
        photo_timings = []
        
        total_count = len(unprocessed)
        for photo_id, photo_path, filename, date_taken in unprocessed:
            # Start timing for this photo
            photo_start_time = time.time()
            
            # Cooperative cancellation
            if stop_event is not None and getattr(stop_event, 'is_set', None) and stop_event.is_set():
                print("⏹️  Processing cancelled by user")
                break
            # Notify UI/CLI before starting this photo
            if callable(progress_callback):
                try:
                    progress_callback(processed_count + 1, total_count, filename)
                except Exception:
                    # Best-effort progress; ignore callback errors
                    pass
            if not os.path.exists(photo_path):
                print(f"❌ File not found: {filename}")
                self.db.mark_photo_processed(photo_id)
                continue
            
            try:
                # Get EXIF orientation information
                exif_orientation = EXIFOrientationHandler.get_exif_orientation(photo_path)
                
                # Process with DeepFace
                if self.verbose >= 1:
                    print(f"📸 Processing: {filename}")
                    if exif_orientation and exif_orientation != 1:
                        print(f"   📐 EXIF orientation: {exif_orientation} ({EXIFOrientationHandler._get_orientation_description(exif_orientation)})")
                elif self.verbose == 0:
                    print(".", end="", flush=True)
                
                if self.verbose >= 2:
                    print(f"   🔍 Using DeepFace: detector={self.detector_backend}, model={self.model_name}")
                
                # Apply EXIF orientation correction before face detection
                corrected_image, original_orientation = EXIFOrientationHandler.correct_image_orientation_from_path(photo_path)
                
                if corrected_image is not None and original_orientation and original_orientation != 1:
                    # Save corrected image temporarily for DeepFace processing
                    import tempfile
                    temp_dir = tempfile.gettempdir()
                    temp_filename = f"corrected_{photo_id}_{filename}"
                    temp_path = os.path.join(temp_dir, temp_filename)
                    corrected_image.save(temp_path, "JPEG", quality=95)
                    
                    # Use corrected image for face detection
                    face_detection_path = temp_path
                    if self.verbose >= 2:
                        print(f"   📐 Using EXIF-corrected image for face detection (orientation {original_orientation})")
                else:
                    # Use original image if no correction needed
                    face_detection_path = photo_path
                
                # Step 1: Use RetinaFace directly for detection + landmarks (with graceful fallback)
                pose_faces = []
                if self.pose_detector:
                    try:
                        pose_faces = self.pose_detector.detect_pose_faces(face_detection_path)
                        if self.verbose >= 2 and pose_faces:
                            print(f"   📐 Pose detection: found {len(pose_faces)} faces with pose data")
                    except Exception as e:
                        if self.verbose >= 1:
                            print(f"⚠️  Pose detection failed for {filename}: {e}, using defaults")
                        pose_faces = []
                
                # Step 2: Use DeepFace for encoding generation
                deepface_start_time = time.time()
                results = DeepFace.represent(
                    img_path=face_detection_path,
                    model_name=self.model_name,
                    detector_backend=self.detector_backend,
                    enforce_detection=DEEPFACE_ENFORCE_DETECTION,
                    align=DEEPFACE_ALIGN_FACES
                )
                deepface_time = time.time() - deepface_start_time
                total_face_detection_time += deepface_time
                
                # Clean up temporary file if created
                if 'temp_path' in locals() and os.path.exists(temp_path):
                    try:
                        os.remove(temp_path)
                    except:
                        pass
                
                if not results:
                    if self.verbose >= 1:
                        print(f"   👤 No faces found (DeepFace: {deepface_time:.2f}s)")
                    # Mark as processed even with no faces
                    self.db.mark_photo_processed(photo_id)
                    processed_count += 1
                    # Record timing for this photo
                    photo_time = time.time() - photo_start_time
                    photo_timings.append({
                        'filename': filename,
                        'total_time': photo_time,
                        'deepface_time': deepface_time,
                        'faces_found': 0
                    })
                    continue
                
                faces_found = len(results)
                total_faces_found += faces_found
                
                if self.verbose >= 1:
                    print(f"   👤 Found {faces_found} faces (DeepFace: {deepface_time:.2f}s)")
                
                # Process each detected face
                for i, result in enumerate(results):
                    # Check cancellation within inner loop
                    if stop_event is not None and getattr(stop_event, 'is_set', None) and stop_event.is_set():
                        print("⏹️  Processing cancelled by user")
                        break
                    
                    # Extract face region info from DeepFace result
                    facial_area = result.get('facial_area', {})
                    face_confidence = result.get('face_confidence', 0.0)
                    embedding = np.array(result['embedding'])
                    
                    # Convert DeepFace facial_area {x, y, w, h} to our location format
                    location = {
                        'x': facial_area.get('x', 0),
                        'y': facial_area.get('y', 0),
                        'w': facial_area.get('w', 0),
                        'h': facial_area.get('h', 0)
                    }
                    
                    # Apply filtering to reduce false positives
                    if not self._is_valid_face_detection(face_confidence, location):
                        if self.verbose >= 2:
                            print(f"      Face {i+1}: Filtered out (confidence: {face_confidence:.3f}, size: {location['w']}x{location['h']})")
                        continue
                    
                    # Calculate face quality score using DeepFace format directly
                    face_location_dict = {
                        'x': facial_area.get('x', 0),
                        'y': facial_area.get('y', 0),
                        'w': facial_area.get('w', 0),
                        'h': facial_area.get('h', 0)
                    }
                    
                    # Load image for quality calculation
                    image = Image.open(photo_path)
                    image_np = np.array(image)
                    quality_score = self._calculate_face_quality_score(image_np, face_location_dict)
                    
                    # Step 3: Match RetinaFace results with DeepFace results
                    pose_info = self._find_matching_pose_info(facial_area, pose_faces)
                    yaw_angle = pose_info.get('yaw_angle')
                    pitch_angle = pose_info.get('pitch_angle')
                    roll_angle = pose_info.get('roll_angle')
                    face_width = pose_info.get('face_width')  # Extract face width for verification
                    
                    # Get landmarks from matched pose_face for storage and pose classification
                    landmarks_json = None
                    matched_pose_face = self._find_matching_pose_face(facial_area, pose_faces)
                    landmarks = matched_pose_face.get('landmarks') if matched_pose_face else None
                    
                    # Recalculate pose_mode using updated logic (check face_width even when yaw is available)
                    from src.utils.pose_detection import PoseDetector
                    pose_mode = PoseDetector.classify_pose_mode(
                        yaw_angle, pitch_angle, roll_angle, face_width
                    )
                    if matched_pose_face and matched_pose_face.get('landmarks'):
                        import json
                        landmarks_dict = matched_pose_face.get('landmarks')
                        # Convert tuple coordinates to lists and numpy types to native Python types for JSON serialization
                        landmarks_serializable = {}
                        for key, value in landmarks_dict.items():
                            if isinstance(value, (tuple, list)):
                                # Convert tuple/list and handle numpy types within
                                landmarks_serializable[key] = [
                                    float(v) if isinstance(v, np.floating) else int(v) if isinstance(v, np.integer) else v
                                    for v in value
                                ]
                            elif isinstance(value, np.floating):
                                # Convert numpy float types to native Python float
                                landmarks_serializable[key] = float(value)
                            elif isinstance(value, np.integer):
                                # Convert numpy integer types to native Python int
                                landmarks_serializable[key] = int(value)
                            else:
                                landmarks_serializable[key] = value
                        landmarks_json = json.dumps(landmarks_serializable)
                    
                    # Log face width for profile detection verification
                    if self.verbose >= 2 and face_width is not None:
                        profile_status = "PROFILE" if face_width < 25.0 else "FRONTAL"
                        print(f"      Face {i+1}: face_width={face_width:.2f}px, pose_mode={pose_mode} ({profile_status})")
                    elif self.verbose >= 3:
                        # Even more verbose: show all pose info
                        print(f"      Face {i+1} pose info: yaw={yaw_angle:.1f}°, pitch={pitch_angle:.1f}°, roll={roll_angle:.1f}°, width={face_width:.2f}px, mode={pose_mode}")
                    
                    # Store in database with DeepFace format, EXIF orientation, and pose data
                    self.db.add_face(
                        photo_id=photo_id,
                        encoding=embedding.tobytes(),
                        location=str(location),  # Store as string representation of dict
                        confidence=0.0,  # Legacy field
                        quality_score=quality_score,
                        person_id=None,
                        detector_backend=self.detector_backend,
                        model_name=self.model_name,
                        face_confidence=face_confidence,
                        exif_orientation=exif_orientation,
                        pose_mode=pose_mode,
                        yaw_angle=yaw_angle,
                        pitch_angle=pitch_angle,
                        roll_angle=roll_angle,
                        landmarks=landmarks_json
                    )
                    
                    if self.verbose >= 3:
                        print(f"      Face {i+1}: {location} (quality: {quality_score:.2f}, confidence: {face_confidence:.2f})")
                
                # Mark as processed
                self.db.mark_photo_processed(photo_id)
                processed_count += 1
                
                # Record timing for this photo
                photo_time = time.time() - photo_start_time
                photo_timings.append({
                    'filename': filename,
                    'total_time': photo_time,
                    'deepface_time': deepface_time,
                    'faces_found': faces_found
                })
                
            except Exception as e:
                print(f"❌ Error processing {filename}: {e}")
                self.db.mark_photo_processed(photo_id)
                # Record timing even for failed photos
                photo_time = time.time() - photo_start_time
                photo_timings.append({
                    'filename': filename,
                    'total_time': photo_time,
                    'deepface_time': 0.0,
                    'faces_found': 0
                })
        
        if self.verbose == 0:
            print()  # New line after dots
        
        # Calculate comprehensive timing statistics
        overall_time = time.time() - overall_start_time
        
        # Print detailed timing summary
        print(f"✅ Processed {processed_count} photos")
        print(f"\n📊 PERFORMANCE SUMMARY:")
        print(f"   ⏱️  Total processing time: {overall_time:.2f}s")
        print(f"   📸 Photos processed: {processed_count}")
        print(f"   👤 Total faces found: {total_faces_found}")
        print(f"   🔍 Total DeepFace time: {total_face_detection_time:.2f}s")
        
        if processed_count > 0:
            avg_time_per_photo = overall_time / processed_count
            avg_deepface_time = total_face_detection_time / processed_count
            print(f"   📈 Average time per photo: {avg_time_per_photo:.2f}s")
            print(f"   🔍 Average DeepFace time per photo: {avg_deepface_time:.2f}s")
            
            if total_faces_found > 0:
                avg_time_per_face = overall_time / total_faces_found
                print(f"   👤 Average time per face: {avg_time_per_face:.2f}s")
        
        # Show per-photo timing details if verbose
        if self.verbose >= 1 and photo_timings:
            print(f"\n📋 PER-PHOTO TIMING DETAILS:")
            for timing in photo_timings:
                print(f"   📸 {timing['filename']}: {timing['total_time']:.2f}s total, {timing['deepface_time']:.2f}s DeepFace, {timing['faces_found']} faces")
        
        # Show slowest photos if there are many
        if len(photo_timings) > 5:
            slowest_photos = sorted(photo_timings, key=lambda x: x['total_time'], reverse=True)[:3]
            print(f"\n🐌 SLOWEST PHOTOS:")
            for timing in slowest_photos:
                print(f"   📸 {timing['filename']}: {timing['total_time']:.2f}s")
        
        return processed_count
    
    def cleanup_false_positive_faces(self, verbose: bool = True) -> int:
        """Remove faces that are likely false positives based on improved filtering criteria
        
        This method can be used to clean up existing false positives in the database
        after improving the face detection filtering.
        
        Returns:
            Number of faces removed
        """
        if verbose:
            print("🧹 Cleaning up false positive faces...")
        
        removed_count = 0
        
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            
            # Get all faces with their metadata
            cursor.execute('''
                SELECT id, location, face_confidence, quality_score, detector_backend, model_name
                FROM faces
                WHERE person_id IS NULL
            ''')
            
            faces_to_check = cursor.fetchall()
            
            if verbose:
                print(f"   Checking {len(faces_to_check)} unidentified faces...")
            
            for face_id, location_str, face_confidence, quality_score, detector_backend, model_name in faces_to_check:
                try:
                    # Parse location string back to dict (DeepFace format only)
                    import ast
                    location = ast.literal_eval(location_str) if isinstance(location_str, str) else location_str
                    
                    # Ensure we have DeepFace format
                    if not isinstance(location, dict):
                        print(f"   ⚠️  Face {face_id} has non-dict location format, skipping")
                        continue
                    
                    # Apply the same validation logic
                    if not self._is_valid_face_detection(face_confidence or 0.0, location):
                        # This face would be filtered out by current criteria, remove it
                        cursor.execute('DELETE FROM faces WHERE id = ?', (face_id,))
                        removed_count += 1
                        
                        if verbose and removed_count <= 10:  # Show first 10 removals
                            print(f"   Removed face {face_id}: confidence={face_confidence:.2f}, size={location.get('w', 0)}x{location.get('h', 0)}")
                        elif verbose and removed_count == 11:
                            print("   ... (showing first 10 removals)")
                
                except Exception as e:
                    if verbose:
                        print(f"   ⚠️  Error checking face {face_id}: {e}")
                    continue
            
            conn.commit()
        
        if verbose:
            print(f"✅ Removed {removed_count} false positive faces")
        
        return removed_count
    
    def _is_valid_face_detection(self, face_confidence: float, location: dict) -> bool:
        """Validate if a face detection is likely to be a real face (not a false positive)"""
        try:
            # Check confidence threshold - be more strict
            if face_confidence < MIN_FACE_CONFIDENCE:
                return False
            
            # Check face size
            width = location.get('w', 0)
            height = location.get('h', 0)
            
            # Too small faces are likely false positives (balloons, decorations, etc.)
            if width < MIN_FACE_SIZE or height < MIN_FACE_SIZE:
                return False
            
            # Too large faces might be full-image false positives
            if width > MAX_FACE_SIZE or height > MAX_FACE_SIZE:
                return False
            
            # Check aspect ratio - faces should be roughly square (not too wide/tall)
            aspect_ratio = width / height if height > 0 else 1.0
            if aspect_ratio < 0.4 or aspect_ratio > 2.5:  # More strict aspect ratio (was 0.3-3.0)
                return False
            
            # Additional filtering for very small faces with low confidence
            # Small faces need higher confidence to be accepted
            face_area = width * height
            if face_area < 6400:  # Less than 80x80 pixels (lowered from 100x100)
                if face_confidence < 0.6:  # Require 60% confidence for small faces (lowered from 80%)
                    return False
            
            # Filter out faces that are too close to image edges (often false positives)
            x = location.get('x', 0)
            y = location.get('y', 0)
            # If face is very close to edges, require higher confidence
            if x < 10 or y < 10:  # Within 10 pixels of top/left edge
                if face_confidence < 0.65:  # Require 65% confidence for edge faces (lowered from 85%)
                    return False
            
            return True
            
        except Exception as e:
            if self.verbose >= 2:
                print(f"⚠️  Error validating face detection: {e}")
            return True  # Default to accepting on error
    
    def _calculate_face_quality_score(self, image: np.ndarray, face_location: dict) -> float:
        """Calculate face quality score based on multiple factors"""
        try:
            # DeepFace format: {x, y, w, h}
            x = face_location.get('x', 0)
            y = face_location.get('y', 0)
            w = face_location.get('w', 0)
            h = face_location.get('h', 0)
            
            face_height = h
            face_width = w
            left = x
            right = x + w
            top = y
            bottom = y + h
            
            # Basic size check - faces too small get lower scores
            min_face_size = 50
            size_score = min(1.0, (face_height * face_width) / (min_face_size * min_face_size))
            
            # Extract face region
            face_region = image[top:bottom, left:right]
            if face_region.size == 0:
                return 0.0
            
            # Convert to grayscale for analysis
            if len(face_region.shape) == 3:
                gray_face = np.mean(face_region, axis=2)
            else:
                gray_face = face_region
            
            # Calculate sharpness (Laplacian variance)
            laplacian_var = np.var(np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]]).astype(np.float32))
            if laplacian_var > 0:
                sharpness = np.var(np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]]).astype(np.float32))
            else:
                sharpness = 0.0
            sharpness_score = min(1.0, sharpness / 1000.0)  # Normalize sharpness
            
            # Calculate brightness and contrast
            mean_brightness = np.mean(gray_face)
            brightness_score = 1.0 - abs(mean_brightness - 128) / 128.0  # Prefer middle brightness
            
            contrast = np.std(gray_face)
            contrast_score = min(1.0, contrast / 64.0)  # Prefer good contrast
            
            # Calculate aspect ratio (faces should be roughly square)
            aspect_ratio = face_width / face_height if face_height > 0 else 1.0
            aspect_score = 1.0 - abs(aspect_ratio - 1.0)  # Prefer square faces
            
            # Calculate position in image (centered faces are better)
            image_height, image_width = image.shape[:2]
            center_x = (left + right) / 2
            center_y = (top + bottom) / 2
            position_x_score = 1.0 - abs(center_x - image_width / 2) / (image_width / 2)
            position_y_score = 1.0 - abs(center_y - image_height / 2) / (image_height / 2)
            position_score = (position_x_score + position_y_score) / 2.0
            
            # Weighted combination of all factors
            quality_score = (
                size_score * 0.25 +
                sharpness_score * 0.25 +
                brightness_score * 0.15 +
                contrast_score * 0.15 +
                aspect_score * 0.10 +
                position_score * 0.10
            )
            
            return max(0.0, min(1.0, quality_score))
            
        except Exception as e:
            if self.verbose >= 2:
                print(f"⚠️  Error calculating face quality: {e}")
            return 0.5  # Default medium quality on error
    
    def _find_matching_pose_face(self, facial_area: Dict, 
                                  pose_faces: List[Dict]) -> Optional[Dict]:
        """Find the matching pose_face object for a given facial_area
        
        Args:
            facial_area: DeepFace facial_area {'x': x, 'y': y, 'w': w, 'h': h}
            pose_faces: List of RetinaFace detection results with pose info
        
        Returns:
            Matched pose_face dictionary, or None if no match
        """
        if not pose_faces:
            return None
        
        deepface_center_x = facial_area.get('x', 0) + facial_area.get('w', 0) / 2
        deepface_center_y = facial_area.get('y', 0) + facial_area.get('h', 0) / 2
        
        best_match = None
        min_distance = float('inf')
        
        for pose_face in pose_faces:
            pose_area = pose_face.get('facial_area', {})
            
            # Handle both dict and list formats (for robustness)
            if isinstance(pose_area, list) and len(pose_area) >= 4:
                # Convert list [x, y, w, h] to dict format
                pose_area = {
                    'x': pose_area[0],
                    'y': pose_area[1],
                    'w': pose_area[2],
                    'h': pose_area[3]
                }
            elif not isinstance(pose_area, dict):
                # Skip if not dict or list
                continue
            
            pose_center_x = (pose_area.get('x', 0) + 
                             pose_area.get('w', 0) / 2)
            pose_center_y = (pose_area.get('y', 0) + 
                             pose_area.get('h', 0) / 2)
            
            # Calculate distance between centers
            distance = ((deepface_center_x - pose_center_x) ** 2 + 
                       (deepface_center_y - pose_center_y) ** 2) ** 0.5
            
            if distance < min_distance:
                min_distance = distance
                best_match = pose_face
        
        # If match is close enough (within 50 pixels), return it
        if best_match and min_distance < 50:
            return best_match
        
        return None
    
    def _find_matching_pose_info(self, facial_area: Dict, 
                                  pose_faces: List[Dict]) -> Dict:
        """Match DeepFace result with RetinaFace pose detection result
        
        Args:
            facial_area: DeepFace facial_area {'x': x, 'y': y, 'w': w, 'h': h}
            pose_faces: List of RetinaFace detection results with pose info
        
        Returns:
            Dictionary with pose information, or defaults
        """
        matched_pose_face = self._find_matching_pose_face(facial_area, pose_faces)
        
        if matched_pose_face:
            return {
                'pose_mode': matched_pose_face.get('pose_mode', 'frontal'),
                'yaw_angle': matched_pose_face.get('yaw_angle'),
                'pitch_angle': matched_pose_face.get('pitch_angle'),
                'roll_angle': matched_pose_face.get('roll_angle'),
                'face_width': matched_pose_face.get('face_width')  # Extract face width for verification
            }
        
        return {
            'pose_mode': 'frontal',
            'yaw_angle': None,
            'pitch_angle': None,
            'roll_angle': None,
            'face_width': None
        }
    
    def _extract_face_crop(self, photo_path: str, location: dict, face_id: int) -> str:
        """Extract and save individual face crop for identification with EXIF orientation correction"""
        try:
            # Check cache first
            cache_key = f"{photo_path}_{location}_{face_id}"
            if cache_key in self._image_cache:
                cached_path = self._image_cache[cache_key]
                # Verify the cached file still exists
                if os.path.exists(cached_path):
                    return cached_path
                else:
                    # Remove from cache if file doesn't exist
                    del self._image_cache[cache_key]
            
            # Parse location from string format (DeepFace format only)
            if isinstance(location, str):
                import ast
                location = ast.literal_eval(location)
            
            # DeepFace format: {x, y, w, h}
            if not isinstance(location, dict):
                raise ValueError(f"Expected DeepFace dict format, got {type(location)}")
            
            # Get EXIF orientation from database
            face_info = self.db.get_face_photo_info(face_id)
            exif_orientation = face_info[3] if face_info and len(face_info) > 3 else None
            
            # Load the image with EXIF orientation correction
            # Since we now apply correction before face detection, we need to apply it here too
            corrected_image, original_orientation = EXIFOrientationHandler.correct_image_orientation_from_path(photo_path)
            if corrected_image is not None and original_orientation and original_orientation != 1:
                # Only apply correction if orientation is not 1 (normal)
                image = corrected_image
                # Use the detected orientation if not stored in database
                if exif_orientation is None:
                    exif_orientation = original_orientation
            else:
                # Use original image if no correction needed or correction fails
                image = Image.open(photo_path)
            
            # Transform face coordinates if image was rotated
            # TEMPORARILY DISABLED FOR TESTING - coordinate transformation might be causing issues
            # if exif_orientation and exif_orientation != 1:
            #     # Get original image dimensions for coordinate transformation
            #     with Image.open(photo_path) as original_image:
            #         original_width, original_height = original_image.size
            #     
            #     # Transform coordinates based on orientation correction
            #     location = EXIFOrientationHandler.transform_face_coordinates(
            #         location, original_width, original_height, exif_orientation
            #     )
            
            left = location.get('x', 0)
            top = location.get('y', 0)
            width = location.get('w', 0)
            height = location.get('h', 0)
            right = left + width
            bottom = top + height
            
            # Add padding around the face (20% of face size)
            face_width = right - left
            face_height = bottom - top
            padding_x = int(face_width * 0.2)
            padding_y = int(face_height * 0.2)
            
            # Calculate crop bounds with padding
            crop_left = max(0, left - padding_x)
            crop_top = max(0, top - padding_y)
            crop_right = min(image.width, right + padding_x)
            crop_bottom = min(image.height, bottom + padding_y)
            
            # Crop the face
            face_crop = image.crop((crop_left, crop_top, crop_right, crop_bottom))
            
            # Create temporary file for the face crop
            temp_dir = tempfile.gettempdir()
            face_filename = f"face_{face_id}_crop.jpg"
            face_path = os.path.join(temp_dir, face_filename)
            
            # Resize for better viewing (minimum 200px width)
            if face_crop.width < 200:
                ratio = 200 / face_crop.width
                new_width = 200
                new_height = int(face_crop.height * ratio)
                face_crop = face_crop.resize((new_width, new_height), Image.Resampling.LANCZOS)
            
            face_crop.save(face_path, "JPEG", quality=95)
            
            # Cache the result
            self._image_cache[cache_key] = face_path
            return face_path
            
        except Exception as e:
            if self.verbose >= 1:
                print(f"⚠️  Could not extract face crop: {e}")
            return None
    
    def _create_comparison_image(self, unid_crop_path: str, match_crop_path: str, person_name: str, confidence: float) -> str:
        """Create a side-by-side comparison image"""
        try:
            # Load both face crops
            unid_img = Image.open(unid_crop_path)
            match_img = Image.open(match_crop_path)
            
            # Resize both to same height for better comparison
            target_height = 300
            unid_ratio = target_height / unid_img.height
            match_ratio = target_height / match_img.height
            
            unid_resized = unid_img.resize((int(unid_img.width * unid_ratio), target_height), Image.Resampling.LANCZOS)
            match_resized = match_img.resize((int(match_img.width * match_ratio), target_height), Image.Resampling.LANCZOS)
            
            # Create comparison image
            total_width = unid_resized.width + match_resized.width + 20  # 20px gap
            comparison = Image.new('RGB', (total_width, target_height + 60), 'white')
            
            # Paste images
            comparison.paste(unid_resized, (0, 30))
            comparison.paste(match_resized, (unid_resized.width + 20, 30))
            
            # Add labels
            draw = ImageDraw.Draw(comparison)
            try:
                # Try to use a font
                font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16)
            except:
                font = ImageFont.load_default()
            
            draw.text((10, 5), "UNKNOWN", fill='red', font=font)
            draw.text((unid_resized.width + 30, 5), f"{person_name.upper()}", fill='green', font=font)
            draw.text((10, target_height + 35), f"Confidence: {confidence:.1%}", fill='blue', font=font)
            
            # Save comparison image
            temp_dir = tempfile.gettempdir()
            comparison_path = os.path.join(temp_dir, f"face_comparison_{person_name}.jpg")
            comparison.save(comparison_path, "JPEG", quality=95)
            
            return comparison_path
            
        except Exception as e:
            if self.verbose >= 1:
                print(f"⚠️  Could not create comparison image: {e}")
            return None
    
    def _get_confidence_description(self, confidence_pct: float) -> str:
        """Get human-readable confidence description"""
        if confidence_pct >= 80:
            return "🟢 (Very High)"
        elif confidence_pct >= 70:
            return "🟡 (High)"
        elif confidence_pct >= 60:
            return "🟠 (Medium)"
        elif confidence_pct >= 50:
            return "🔴 (Low)"
        else:
            return "⚫ (Very Low)"
    
    def _calculate_cosine_similarity(self, encoding1: np.ndarray, encoding2: np.ndarray) -> float:
        """Calculate cosine similarity distance between two face encodings
        
        Returns distance value (0 = identical, 2 = opposite) for compatibility with face_recognition API.
        Uses cosine similarity internally which is better for DeepFace embeddings.
        """
        try:
            # Ensure encodings are numpy arrays
            enc1 = np.array(encoding1).flatten()
            enc2 = np.array(encoding2).flatten()
            
            # Check if encodings have the same length
            if len(enc1) != len(enc2):
                if self.verbose >= 2:
                    print(f"⚠️  Encoding length mismatch: {len(enc1)} vs {len(enc2)}")
                return 2.0  # Maximum distance on mismatch
            
            # Normalize encodings
            enc1_norm = enc1 / (np.linalg.norm(enc1) + 1e-8)
            enc2_norm = enc2 / (np.linalg.norm(enc2) + 1e-8)
            
            # Calculate cosine similarity
            cosine_sim = np.dot(enc1_norm, enc2_norm)
            
            # Clamp to valid range [-1, 1]
            cosine_sim = np.clip(cosine_sim, -1.0, 1.0)
            
            # Convert to distance (0 = identical, 2 = opposite)
            # For consistency with face_recognition's distance metric
            distance = 1.0 - cosine_sim  # Range [0, 2], where 0 is perfect match
            
            return distance
            
        except Exception as e:
            if self.verbose >= 1:
                print(f"⚠️  Error calculating similarity: {e}")
            return 2.0  # Maximum distance on error
    
    def _calibrate_confidence(self, distance: float, tolerance: float = None) -> float:
        """Convert distance to calibrated confidence percentage (actual match probability)
        
        This uses empirical calibration based on DeepFace ArcFace model characteristics.
        The calibration accounts for the non-linear relationship between distance and match probability.
        
        Args:
            distance: Cosine distance (0 = identical, 2 = opposite)
            tolerance: Matching tolerance threshold (default: DEFAULT_FACE_TOLERANCE)
            
        Returns:
            Calibrated confidence percentage (0-100) representing actual match probability
        """
        if tolerance is None:
            tolerance = DEFAULT_FACE_TOLERANCE
        
        # Use configuration setting to determine calibration method
        if not USE_CALIBRATED_CONFIDENCE:
            # Fallback to simple linear transformation (old behavior)
            return max(0, min(100, (1 - distance) * 100))
        
        if CONFIDENCE_CALIBRATION_METHOD == "linear":
            # Simple linear transformation (old behavior)
            return max(0, min(100, (1 - distance) * 100))
        
        elif CONFIDENCE_CALIBRATION_METHOD == "sigmoid":
            # Sigmoid-based calibration
            # Maps distance to probability using sigmoid function
            normalized_distance = distance / tolerance
            sigmoid_factor = 1 / (1 + np.exp(5 * (normalized_distance - 1)))
            return max(1, min(100, sigmoid_factor * 100))
        
        else:  # "empirical" - default method
            # Empirical calibration parameters for DeepFace ArcFace model
            # These are derived from analysis of distance distributions for matching/non-matching pairs
            
            # For distances well below threshold: high confidence
            if distance <= tolerance * 0.5:
                # Very close matches: exponential decay from 100%
                confidence = 100 * np.exp(-distance * 2.5)
                return min(100, max(95, confidence))
            
            # For distances near threshold: moderate confidence  
            elif distance <= tolerance:
                # Near-threshold matches: sigmoid-like curve
                # Maps distance to probability based on empirical data
                normalized_distance = (distance - tolerance * 0.5) / (tolerance * 0.5)
                confidence = 95 - (normalized_distance * 40)  # 95% to 55% range
                return max(55, min(95, confidence))
            
            # For distances above threshold: low confidence
            elif distance <= tolerance * 1.5:
                # Above threshold but not too far: rapid decay
                normalized_distance = (distance - tolerance) / (tolerance * 0.5)
                confidence = 55 - (normalized_distance * 35)  # 55% to 20% range
                return max(20, min(55, confidence))
            
            # For very large distances: very low confidence
            else:
                # Very far matches: very low probability
                confidence = 20 * np.exp(-(distance - tolerance * 1.5) * 1.5)
                return max(1, min(20, confidence))
    
    def _get_calibrated_confidence(self, distance: float, tolerance: float = None) -> Tuple[float, str]:
        """Get calibrated confidence percentage and description
        
        Args:
            distance: Cosine distance between face encodings
            tolerance: Matching tolerance threshold
            
        Returns:
            Tuple of (calibrated_confidence_pct, description)
        """
        calibrated_pct = self._calibrate_confidence(distance, tolerance)
        description = self._get_confidence_description(calibrated_pct)
        return calibrated_pct, description
    
    def _calculate_adaptive_tolerance(self, base_tolerance: float, face_quality: float, match_confidence: float = None) -> float:
        """Calculate adaptive tolerance based on face quality and match confidence
        
        Note: For DeepFace, tolerance values are generally lower than face_recognition
        """
        # Start with base tolerance (e.g., 0.4 instead of 0.6 for DeepFace)
        tolerance = base_tolerance
        
        # Adjust based on face quality (higher quality = stricter tolerance)
        quality_factor = 0.9 + (face_quality * 0.2)  # Range: 0.9 to 1.1
        tolerance *= quality_factor
        
        # If we have match confidence, adjust further
        if match_confidence is not None:
            # Higher confidence matches can use stricter tolerance
            confidence_factor = 0.95 + (match_confidence * 0.1)
            tolerance *= confidence_factor
        
        # Ensure tolerance stays within reasonable bounds for DeepFace
        return max(0.2, min(0.6, tolerance))  # Lower range for DeepFace
    
    def _get_filtered_similar_faces(self, face_id: int, tolerance: float, include_same_photo: bool = False, face_status: dict = None) -> List[Dict]:
        """Get similar faces with consistent filtering and sorting logic used by both auto-match and identify"""
        # Find similar faces using the core function
        similar_faces_data = self.find_similar_faces(face_id, tolerance=tolerance, include_same_photo=include_same_photo)
        
        # Filter to only show unidentified faces with confidence filtering
        filtered_faces = []
        for face in similar_faces_data:
            # For auto-match: only filter by database state (keep existing behavior)
            # For identify: also filter by current session state
            is_identified_in_db = face.get('person_id') is not None
            is_identified_in_session = face_status and face.get('face_id') in face_status and face_status[face.get('face_id')] == 'identified'
            
            # If face_status is provided (identify mode), use both filters
            # If face_status is None (auto-match mode), only use database filter
            if face_status is not None:
                # Identify mode: filter out both database and session identified faces
                if not is_identified_in_db and not is_identified_in_session:
                    # Calculate confidence percentage
                    confidence_pct, _ = self._get_calibrated_confidence(face['distance'])
                    
                    # Only include matches with reasonable confidence (at least 40%)
                    if confidence_pct >= 40:
                        filtered_faces.append(face)
            else:
                # Auto-match mode: only filter by database state (keep existing behavior)
                if not is_identified_in_db:
                    # Calculate confidence percentage
                    confidence_pct, _ = self._get_calibrated_confidence(face['distance'])
                    
                    # Only include matches with reasonable confidence (at least 40%)
                    if confidence_pct >= 40:
                        filtered_faces.append(face)
        
        # Sort by confidence (distance) - highest confidence first
        filtered_faces.sort(key=lambda x: x['distance'])
        
        return filtered_faces
    
    def _filter_unique_faces(self, faces: List[Dict]) -> List[Dict]:
        """Filter faces to show only unique ones, hiding duplicates with high/medium confidence matches"""
        if not faces:
            return faces
        
        unique_faces = []
        seen_face_groups = set()  # Track face groups that have been seen
        
        for face in faces:
            face_id = face['face_id']
            confidence_pct = (1 - face['distance']) * 100
            
            # Only consider high (>=70%) or medium (>=60%) confidence matches for grouping
            if confidence_pct >= 60:
                # Find all faces that match this one with high/medium confidence
                matching_face_ids = set()
                for other_face in faces:
                    other_face_id = other_face['face_id']
                    other_confidence_pct = (1 - other_face['distance']) * 100
                    
                    # If this face matches the current face with high/medium confidence
                    if other_confidence_pct >= 60:
                        matching_face_ids.add(other_face_id)
                
                # Create a sorted tuple to represent this group of matching faces
                face_group = tuple(sorted(matching_face_ids))
                
                # Only show this face if we haven't seen this group before
                if face_group not in seen_face_groups:
                    seen_face_groups.add(face_group)
                    unique_faces.append(face)
            else:
                # For low confidence matches, always show them (they're likely different people)
                unique_faces.append(face)
        
        return unique_faces
    
    def find_similar_faces(self, face_id: int = None, tolerance: float = DEFAULT_FACE_TOLERANCE, include_same_photo: bool = False) -> List[Dict]:
        """Find similar faces across all photos with improved multi-encoding and quality scoring"""
        if face_id:
            # Find faces similar to a specific face
            target_face = self.db.get_face_encodings(face_id)
            if not target_face:
                print(f"❌ Face ID {face_id} not found")
                return []
            
            target_encoding = self._get_cached_face_encoding(face_id, target_face)
            
            # Get all other faces with quality scores
            all_faces = self.db.get_all_face_encodings()
            matches = []
            
            # Compare target face with all other faces using adaptive tolerance
            for face_data in all_faces:
                other_id, other_encoding, other_person_id, other_quality = face_data
                if other_id == face_id:
                    continue
                
                other_enc = self._get_cached_face_encoding(other_id, other_encoding)
                
                # Calculate adaptive tolerance based on both face qualities
                target_quality = 0.5  # Default quality for target face
                avg_quality = (target_quality + other_quality) / 2
                adaptive_tolerance = self._calculate_adaptive_tolerance(tolerance, avg_quality)
                
                distance = self._calculate_cosine_similarity(target_encoding, other_enc)
                if distance <= adaptive_tolerance:
                    # Get photo info for this face
                    photo_info = self.db.get_face_photo_info(other_id)
                    if photo_info:
                        matches.append({
                            'face_id': other_id,
                            'person_id': other_person_id,
                            'distance': distance,
                            'quality_score': other_quality,
                            'adaptive_tolerance': adaptive_tolerance,
                            'photo_id': photo_info[0],
                            'filename': photo_info[1],
                            'location': photo_info[2]
                        })
            
            return matches
        
        else:
            # Find all unidentified faces and try to match them with identified ones
            all_faces = self.db.get_all_face_encodings()
            matches = []
            
            # Auto-match unidentified faces with identified ones using multi-encoding
            identified_faces = [f for f in all_faces if f[2] is not None]  # person_id is not None
            unidentified_faces = [f for f in all_faces if f[2] is None]    # person_id is None
            
            print(f"\n🔍 Auto-matching {len(unidentified_faces)} unidentified faces with {len(identified_faces)} known faces...")
            
            # Group identified faces by person
            person_encodings = {}
            for id_face in identified_faces:
                person_id = id_face[2]
                if person_id not in person_encodings:
                    id_enc = self._get_cached_face_encoding(id_face[0], id_face[1])
                    person_encodings[person_id] = [(id_enc, id_face[3])]
            
            for unid_face in unidentified_faces:
                unid_id, unid_encoding, _, unid_quality = unid_face
                unid_enc = self._get_cached_face_encoding(unid_id, unid_encoding)
                
                best_match = None
                best_distance = float('inf')
                best_person_id = None
                
                # Compare with all person encodings
                for person_id, encodings in person_encodings.items():
                    for person_enc, person_quality in encodings:
                        # Calculate adaptive tolerance based on both face qualities
                        avg_quality = (unid_quality + person_quality) / 2
                        adaptive_tolerance = self._calculate_adaptive_tolerance(tolerance, avg_quality)
                        
                        distance = self._calculate_cosine_similarity(unid_enc, person_enc)
                        
                        if distance <= adaptive_tolerance and distance < best_distance:
                            best_distance = distance
                            best_person_id = person_id
                            
                            best_match = {
                                'unidentified_id': unid_id,
                                'person_id': person_id,
                                'distance': distance,
                                'quality_score': unid_quality,
                                'adaptive_tolerance': adaptive_tolerance
                            }
                
                if best_match:
                    matches.append(best_match)
            
            return matches
    
    def add_person_encoding(self, person_id: int, face_id: int, encoding: np.ndarray, quality_score: float):
        """Add a face encoding to a person's encoding collection"""
        self.db.add_person_encoding(person_id, face_id, encoding.tobytes(), quality_score)
    
    def get_person_encodings(self, person_id: int, min_quality: float = MIN_FACE_QUALITY) -> List[Tuple[np.ndarray, float]]:
        """Get all high-quality encodings for a person"""
        results = self.db.get_person_encodings(person_id, min_quality)
        return [(np.frombuffer(encoding, dtype=np.float64), quality_score) for encoding, quality_score in results]
    
    def update_person_encodings(self, person_id: int):
        """Update person encodings when a face is identified"""
        self.db.update_person_encodings(person_id)
    
    
    def _create_comparison_image(self, unid_crop_path: str, match_crop_path: str, person_name: str, confidence: float) -> str:
        """Create a side-by-side comparison image"""
        try:
            # Load both face crops
            unid_img = Image.open(unid_crop_path)
            match_img = Image.open(match_crop_path)
            
            # Resize both to same height for better comparison
            target_height = 300
            unid_ratio = target_height / unid_img.height
            match_ratio = target_height / match_img.height
            
            unid_resized = unid_img.resize((int(unid_img.width * unid_ratio), target_height), Image.Resampling.LANCZOS)
            match_resized = match_img.resize((int(match_img.width * match_ratio), target_height), Image.Resampling.LANCZOS)
            
            # Create comparison image
            total_width = unid_resized.width + match_resized.width + 20  # 20px gap
            comparison = Image.new('RGB', (total_width, target_height + 60), 'white')
            
            # Paste images
            comparison.paste(unid_resized, (0, 30))
            comparison.paste(match_resized, (unid_resized.width + 20, 30))
            
            # Add labels
            draw = ImageDraw.Draw(comparison)
            try:
                # Try to use a font
                font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16)
            except:
                font = ImageFont.load_default()
            
            draw.text((10, 5), "UNKNOWN", fill='red', font=font)
            draw.text((unid_resized.width + 30, 5), f"{person_name.upper()}", fill='green', font=font)
            draw.text((10, target_height + 35), f"Confidence: {confidence:.1%}", fill='blue', font=font)
            
            # Save comparison image
            temp_dir = tempfile.gettempdir()
            comparison_path = os.path.join(temp_dir, f"face_comparison_{person_name}.jpg")
            comparison.save(comparison_path, "JPEG", quality=95)
            
            return comparison_path
            
        except Exception as e:
            if self.verbose >= 1:
                print(f"⚠️  Could not create comparison image: {e}")
            return None
    
    def _get_confidence_description(self, confidence_pct: float) -> str:
        """Get human-readable confidence description"""
        if confidence_pct >= 80:
            return "🟢 (Very High)"
        elif confidence_pct >= 70:
            return "🟡 (High)"
        elif confidence_pct >= 60:
            return "🟠 (Medium)"
        elif confidence_pct >= 50:
            return "🔴 (Low)"
        else:
            return "⚫ (Very Low)"
    
    def _display_similar_faces_in_panel(self, parent_frame, similar_faces_data, face_vars, face_images, face_crops, current_face_id=None, face_selection_states=None, data_cache=None):
        """Display similar faces in a panel - reuses auto-match display logic"""
        import tkinter as tk
        from tkinter import ttk
        from PIL import Image, ImageTk
        import os
        
        # Create all similar faces using auto-match style display
        for i, face_data in enumerate(similar_faces_data[:10]):  # Limit to 10 faces
            similar_face_id = face_data['face_id']
            filename = face_data['filename']
            distance = face_data['distance']
            quality = face_data.get('quality_score', 0.5)
            
            # Calculate confidence like in auto-match
            confidence_pct, _ = self._get_calibrated_confidence(distance)
            confidence_desc = self._get_confidence_description(confidence_pct)
            
            # Create match frame using auto-match style
            match_frame = ttk.Frame(parent_frame)
            match_frame.pack(fill=tk.X, padx=5, pady=5)
            
            # Checkbox for this match (reusing auto-match checkbox style)
            match_var = tk.BooleanVar()
            face_vars.append((similar_face_id, match_var))
            
            # Restore previous checkbox state if available (auto-match style)
            if current_face_id is not None and face_selection_states is not None:
                unique_key = f"{current_face_id}_{similar_face_id}"
                if current_face_id in face_selection_states and unique_key in face_selection_states[current_face_id]:
                    saved_state = face_selection_states[current_face_id][unique_key]
                    match_var.set(saved_state)
                
                # Add immediate callback to save state when checkbox changes (auto-match style)
                def make_callback(var, face_id, similar_face_id):
                    def on_checkbox_change(*args):
                        unique_key = f"{face_id}_{similar_face_id}"
                        if face_id not in face_selection_states:
                            face_selection_states[face_id] = {}
                        face_selection_states[face_id][unique_key] = var.get()
                    return on_checkbox_change
                
                # Bind the callback to the variable
                match_var.trace('w', make_callback(match_var, current_face_id, similar_face_id))
            
            # Configure match frame for grid layout
            match_frame.columnconfigure(0, weight=0)  # Checkbox column - fixed width
            match_frame.columnconfigure(1, weight=1)  # Text column - expandable
            match_frame.columnconfigure(2, weight=0)  # Image column - fixed width
            
            # Checkbox without text
            checkbox = ttk.Checkbutton(match_frame, variable=match_var)
            checkbox.grid(row=0, column=0, rowspan=2, sticky=(tk.W, tk.N), padx=(0, 5))
            
            # Create labels for confidence and filename
            confidence_label = ttk.Label(match_frame, text=f"{confidence_pct:.1f}% {confidence_desc}", font=("Arial", 9, "bold"))
            confidence_label.grid(row=0, column=1, sticky=tk.W, padx=(0, 10))
            
            filename_label = ttk.Label(match_frame, text=f"📁 {filename}", font=("Arial", 8), foreground="gray")
            filename_label.grid(row=1, column=1, sticky=tk.W, padx=(0, 10))
            
            # Face image (reusing auto-match image display)
            try:
                # Get photo path from cache or database
                photo_path = None
                if data_cache and 'photo_paths' in data_cache:
                    # Find photo path by filename in cache
                    for photo_data in data_cache['photo_paths'].values():
                        if photo_data['filename'] == filename:
                            photo_path = photo_data['path']
                            break
                
                # Fallback to database if not in cache
                if photo_path is None:
                    with self.db.get_db_connection() as conn:
                        cursor = conn.cursor()
                        cursor.execute('SELECT path FROM photos WHERE filename = ?', (filename,))
                        result = cursor.fetchone()
                        photo_path = result[0] if result else None
                
                # Extract face crop using existing method
                face_crop_path = self._extract_face_crop(photo_path, face_data['location'], similar_face_id)
                if face_crop_path and os.path.exists(face_crop_path):
                    face_crops.append(face_crop_path)
                    
                    # Create canvas for face image (like in auto-match)
                    style = ttk.Style()
                    canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
                    match_canvas = tk.Canvas(match_frame, width=80, height=80, bg=canvas_bg_color, highlightthickness=0)
                    match_canvas.grid(row=0, column=2, rowspan=2, sticky=(tk.W, tk.N), padx=(10, 0))
                    
                    # Load and display image (reusing auto-match image loading)
                    pil_image = Image.open(face_crop_path)
                    pil_image.thumbnail((80, 80), Image.Resampling.LANCZOS)
                    photo = ImageTk.PhotoImage(pil_image)
                    match_canvas.create_image(40, 40, image=photo)
                    match_canvas.image = photo  # Keep reference
                    face_images.append(photo)
                    
                    # Add photo icon to the similar face
                    self._create_photo_icon(match_canvas, photo_path, icon_size=15,
                                          face_x=40, face_y=40, 
                                          face_width=80, face_height=80,
                                          canvas_width=80, canvas_height=80)
                else:
                    # No image available
                    match_canvas = tk.Canvas(match_frame, width=80, height=80, bg='white')
                    match_canvas.pack(side=tk.LEFT, padx=(10, 0))
                    match_canvas.create_text(40, 40, text="🖼️", fill="gray")
            except Exception as e:
                # Error loading image
                match_canvas = tk.Canvas(match_frame, width=80, height=80, bg='white')
                match_canvas.pack(side=tk.LEFT, padx=(10, 0))
                match_canvas.create_text(40, 40, text="❌", fill="red")
    
    def _create_photo_icon(self, canvas, photo_path, icon_size=20, icon_x=None, icon_y=None, 
                          canvas_width=None, canvas_height=None, face_x=None, face_y=None, 
                          face_width=None, face_height=None):
        """Create a reusable photo icon with tooltip on a canvas"""
        import tkinter as tk
        import subprocess
        import platform
        import os
        
        def open_source_photo(event):
            """Open the source photo in a properly sized window"""
            try:
                system = platform.system()
                if system == "Windows":
                    # Try to open with a specific image viewer that supports window sizing
                    try:
                        subprocess.run(["mspaint", photo_path], check=False)
                    except:
                        os.startfile(photo_path)
                elif system == "Darwin":  # macOS
                    # Use Preview with specific window size
                    subprocess.run(["open", "-a", "Preview", photo_path])
                else:  # Linux and others
                    # Try common image viewers with window sizing options
                    viewers_to_try = [
                        ["eog", "--new-window", photo_path],  # Eye of GNOME
                        ["gwenview", photo_path],  # KDE image viewer
                        ["feh", "--geometry", "800x600", photo_path],  # feh with specific size
                        ["gimp", photo_path],  # GIMP
                        ["xdg-open", photo_path]  # Fallback to default
                    ]
                    
                    opened = False
                    for viewer_cmd in viewers_to_try:
                        try:
                            result = subprocess.run(viewer_cmd, check=False, capture_output=True)
                            if result.returncode == 0:
                                opened = True
                                break
                        except:
                            continue
                    
                    if not opened:
                        # Final fallback
                        subprocess.run(["xdg-open", photo_path])
            except Exception as e:
                print(f"❌ Could not open photo: {e}")
        
        # Create tooltip for the icon
        tooltip = None
        
        def show_tooltip(event):
            nonlocal tooltip
            if tooltip:
                tooltip.destroy()
            tooltip = tk.Toplevel()
            tooltip.wm_overrideredirect(True)
            tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}")
            label = tk.Label(tooltip, text="Show original photo", 
                           background="lightyellow", relief="solid", borderwidth=1,
                           font=("Arial", 9))
            label.pack()
        
        def hide_tooltip(event):
            nonlocal tooltip
            if tooltip:
                tooltip.destroy()
                tooltip = None
        
        # Calculate icon position
        if icon_x is None or icon_y is None:
            if face_x is not None and face_y is not None and face_width is not None and face_height is not None:
                # Position relative to face image - exactly in the corner
                face_right = face_x + face_width // 2
                face_top = face_y - face_height // 2
                icon_x = face_right - icon_size
                icon_y = face_top
            else:
                # Position relative to canvas - exactly in the corner
                if canvas_width is None:
                    canvas_width = canvas.winfo_width()
                if canvas_height is None:
                    canvas_height = canvas.winfo_height()
                icon_x = canvas_width - icon_size
                icon_y = 0
        
        # Ensure icon stays within canvas bounds
        if canvas_width is None:
            canvas_width = canvas.winfo_width()
        if canvas_height is None:
            canvas_height = canvas.winfo_height()
        icon_x = min(icon_x, canvas_width - icon_size)
        icon_y = max(icon_y, 0)
        
        # Draw the photo icon
        canvas.create_rectangle(icon_x, icon_y, icon_x + icon_size, icon_y + icon_size, 
                              fill="white", outline="black", width=1, tags="photo_icon")
        canvas.create_text(icon_x + icon_size//2, icon_y + icon_size//2, 
                         text="📷", font=("Arial", 10), tags="photo_icon")
        
        # Bind events
        canvas.tag_bind("photo_icon", "<Button-1>", open_source_photo)
        canvas.tag_bind("photo_icon", "<Enter>", lambda e: (canvas.config(cursor="hand2"), show_tooltip(e)))
        canvas.tag_bind("photo_icon", "<Leave>", lambda e: (canvas.config(cursor=""), hide_tooltip(e)))
        canvas.tag_bind("photo_icon", "<Motion>", lambda e: (show_tooltip(e) if tooltip else None))
        
        return tooltip  # Return tooltip reference for cleanup if needed