#!/usr/bin/env python3
"""
Face identification GUI implementation for PunimTag
"""

import os
import time
import tkinter as tk
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
from typing import List, Dict, Tuple, Optional

from config import DEFAULT_BATCH_SIZE, DEFAULT_FACE_TOLERANCE
from database import DatabaseManager
from face_processing import FaceProcessor
from gui_core import GUICore


class IdentifyGUI:
    """Handles the face identification GUI interface"""
    
    def __init__(self, db_manager: DatabaseManager, face_processor: FaceProcessor, verbose: int = 0):
        """Initialize the identify GUI"""
        self.db = db_manager
        self.face_processor = face_processor
        self.verbose = verbose
        self.gui_core = GUICore()
    
    def identify_faces(self, batch_size: int = DEFAULT_BATCH_SIZE, show_faces: bool = False, 
                      tolerance: float = DEFAULT_FACE_TOLERANCE, 
                      date_from: str = None, date_to: str = None, 
                      date_processed_from: str = None, date_processed_to: str = None) -> int:
        """Interactive face identification with optimized performance"""
        
        # Get unidentified faces from database
        unidentified = self._get_unidentified_faces(batch_size, date_from, date_to, 
                                                  date_processed_from, date_processed_to)
        
        if not unidentified:
            print("🎉 All faces have been identified!")
            return 0
        
        print(f"\n👤 Found {len(unidentified)} unidentified faces")
        print("Commands: [name] = identify, 's' = skip, 'q' = quit, 'list' = show people\n")
        
        # Pre-fetch all needed data to avoid repeated database queries
        print("📊 Pre-fetching data for optimal performance...")
        identify_data_cache = self._prefetch_identify_data(unidentified)
        
        print(f"✅ Pre-fetched {len(identify_data_cache.get('photo_paths', {}))} photo paths and {len(identify_data_cache.get('people_names', []))} people names")
        
        identified_count = 0
        
        # Create the main window
        root = tk.Tk()
        root.title("Face Identification")
        root.resizable(True, True)
        
        # Track window state to prevent multiple destroy calls
        window_destroyed = False
        selected_person_id = None
        force_exit = False
        
        # Track current face crop path for cleanup
        current_face_crop_path = None
        
        # Hide window initially to prevent flash at corner
        root.withdraw()
        
        # Set up protocol handler for window close button (X)
        def on_closing():
            nonlocal command, waiting_for_input, window_destroyed, current_face_crop_path, force_exit
            
            
            # First check for selected similar faces without person name
            if not self._validate_navigation(gui_components):
                return  # Cancel close
            
            # Check if there are pending identifications
            pending_identifications = self._get_pending_identifications(face_person_names, face_status)
            
            if pending_identifications:
                # Ask user if they want to save pending identifications
                result = messagebox.askyesnocancel(
                    "Save Pending Identifications?",
                    f"You have {len(pending_identifications)} pending identifications.\n\n"
                    "Do you want to save them before closing?\n\n"
                    "• Yes: Save all pending identifications and close\n"
                    "• No: Close without saving\n"
                    "• Cancel: Return to identification"
                )
                
                if result is True:  # Yes - Save and close
                    identified_count += self._save_all_pending_identifications(face_person_names, face_status, identify_data_cache)
                    # Continue to cleanup and close
                elif result is False:  # No - Close without saving
                    # Continue to cleanup and close
                    pass
                else:  # Cancel - Don't close
                    return  # Exit without cleanup or closing
            
            # Only reach here if user chose Yes, No, or there are no pending identifications
            # Clean up face crops and caches
            self.face_processor.cleanup_face_crops(current_face_crop_path)
            self.db.close_db_connection()
            
            if not window_destroyed:
                window_destroyed = True
                try:
                    root.destroy()
                except tk.TclError:
                    pass  # Window already destroyed
            
            # Force process termination
            force_exit = True
            root.quit()
        
        
        root.protocol("WM_DELETE_WINDOW", on_closing)
        
        # Set up window size saving
        saved_size = self.gui_core.setup_window_size_saving(root)
        
        # Create main frame
        main_frame = ttk.Frame(root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Configure grid weights
        root.columnconfigure(0, weight=1)
        root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(0, weight=1)  # Left panel
        main_frame.columnconfigure(1, weight=1)  # Right panel for similar faces
        # Configure row weights to minimize spacing around Unique checkbox
        main_frame.rowconfigure(2, weight=0)  # Unique checkbox row - no expansion
        main_frame.rowconfigure(3, weight=1)  # Main panels row - expandable
        
        # Photo info
        info_label = ttk.Label(main_frame, text="", font=("Arial", 10, "bold"))
        info_label.grid(row=0, column=0, columnspan=2, pady=(0, 10), sticky=tk.W)
        
        
        # Store face selection states per face ID to preserve selections during navigation
        face_selection_states = {}  # {face_id: {unique_key: bool}}
        
        # Store person names per face ID to preserve names during navigation
        face_person_names = {}  # {face_id: person_name}
        
        # Process each face with back navigation support
        # Keep track of original face list and current position
        original_faces = list(unidentified)  # Make a copy of the original list
        i = 0
        face_status = {}  # Track which faces have been identified
        
        # Button commands
        command = None
        waiting_for_input = False
        
        # Define quit handler as local function (like in old version)
        def on_quit():
            nonlocal command, waiting_for_input, window_destroyed, force_exit
            
            
            # First check for unsaved changes in the form
            validation_result = self._validate_navigation(gui_components)
            if validation_result == 'cancel':
                return  # Cancel quit
            elif validation_result == 'save_and_continue':
                # Save the current identification before proceeding
                # Add the current form data to pending identifications
                current_face_key = list(face_person_names.keys())[0] if face_person_names else None
                if current_face_key:
                    first_name = gui_components['first_name_var'].get().strip()
                    last_name = gui_components['last_name_var'].get().strip()
                    date_of_birth = gui_components['date_of_birth_var'].get().strip()
                    if first_name and last_name and date_of_birth:
                        face_person_names[current_face_key] = {
                            'first_name': first_name,
                            'last_name': last_name,
                            'date_of_birth': date_of_birth
                        }
                        face_status[current_face_key] = 'identified'
            elif validation_result == 'discard_and_continue':
                # Clear the form but don't save
                self._clear_form(gui_components)
            
            # Check if there are pending identifications (faces with complete data but not yet saved)
            pending_identifications = self._get_pending_identifications(face_person_names, face_status)
            
            if pending_identifications:
                # Temporarily disable window close handler to prevent interference
                root.protocol("WM_DELETE_WINDOW", lambda: None)
                
                # Ask user if they want to save pending identifications
                result = messagebox.askyesnocancel(
                    "Save Pending Identifications?",
                    f"You have {len(pending_identifications)} pending identifications.\n\n"
                    "Do you want to save them before quitting?\n\n"
                    "• Yes: Save all pending identifications and quit\n"
                    "• No: Quit without saving\n"
                    "• Cancel: Return to identification"
                )
                
                # Re-enable window close handler
                root.protocol("WM_DELETE_WINDOW", on_closing)
                
                
                if result is True:  # Yes - Save and quit
                    identified_count += self._save_all_pending_identifications(face_person_names, face_status, identify_data_cache)
                    command = 'q'
                    waiting_for_input = False
                elif result is False:  # No - Quit without saving
                    command = 'q'
                    waiting_for_input = False
                else:  # Cancel - Don't quit
                    return  # Exit without any cleanup or window destruction
            else:
                # No pending identifications, quit normally
                command = 'q'
                waiting_for_input = False
            
            # Only reach here if user chose Yes, No, or there are no pending identifications
            # Clean up and close window
            if not window_destroyed:
                window_destroyed = True
                try:
                    root.destroy()
                except tk.TclError:
                    pass  # Window already destroyed
            
            # Force process termination
            force_exit = True
            root.quit()
        
        # Create the GUI components with the quit handler
        gui_components = self._create_gui_components(main_frame, identify_data_cache, 
                                                   date_from, date_to, date_processed_from, date_processed_to, batch_size, on_quit)
        
        # Set up command variable for button callbacks
        self._current_command_var = gui_components['command_var']
        
        # Show the window
        try:
            root.deiconify()
            root.lift()
            root.focus_force()
            
            # Force window to render completely before proceeding
            root.update_idletasks()
            root.update()
            
            # Small delay to ensure canvas is properly rendered
            time.sleep(0.1)
            
            # Schedule the first image update after the window is fully rendered
            def update_first_image():
                try:
                    if i < len(original_faces):
                        face_id, photo_id, photo_path, filename, location = original_faces[i]
                        
                        # Extract face crop if enabled
                        face_crop_path = None
                        if show_faces:
                            face_crop_path = self.face_processor._extract_face_crop(photo_path, location, face_id)
                        
                        # Update the face image
                        self._update_face_image(gui_components, show_faces, face_crop_path, photo_path)
                except Exception as e:
                    print(f"❌ Error updating first image: {e}")
            
            # Schedule the update after a short delay
            root.after(200, update_first_image)
            
        except tk.TclError:
            # Window was destroyed before we could show it
            return 0
        
        # Main processing loop
        while not window_destroyed:
            # Check if current face is identified and update index if needed
            has_faces, i = self._update_current_face_index(original_faces, i, face_status)
            if not has_faces:
                # All faces have been identified
                print("\n🎉 All faces have been identified!")
                break
            
            # Ensure we don't go beyond the bounds
            if i >= len(original_faces):
                # Stay on the last face instead of breaking
                i = len(original_faces) - 1
                
            face_id, photo_id, photo_path, filename, location = original_faces[i]
            
            # Check if this face was already identified in this session
            is_already_identified = face_id in face_status and face_status[face_id] == 'identified'
            
            # Reset command and waiting state for each face
            command = None
            waiting_for_input = True
            
            # Update the display
            current_pos, total_unidentified = self._get_current_face_position(original_faces, i, face_status)
            print(f"\n--- Face {current_pos}/{total_unidentified} (unidentified) ---")
            print(f"📁 Photo: {filename}")
            print(f"📍 Face location: {location}")
            
            # Update title
            root.title(f"Face Identification - {current_pos}/{total_unidentified} (unidentified)")
            
            # Update button states
            self._update_button_states(gui_components, original_faces, i, face_status)
            
            # Update similar faces panel if compare is enabled
            if gui_components['compare_var'].get():
                self._update_similar_faces(gui_components, face_id, tolerance, face_status, 
                                         face_selection_states, identify_data_cache, original_faces, i)
            
            # Update photo info
            if is_already_identified:
                # Get the person name for this face
                person_name = self._get_person_name_for_face(face_id)
                info_label.config(text=f"📁 Photo: {filename} (Face {current_pos}/{total_unidentified}) - ✅ Already identified as: {person_name}")
                print(f"✅ Already identified as: {person_name}")
            else:
                info_label.config(text=f"📁 Photo: {filename} (Face {current_pos}/{total_unidentified})")
            
            # Extract face crop if enabled
            face_crop_path = None
            if show_faces:
                face_crop_path = self.face_processor._extract_face_crop(photo_path, location, face_id)
                if face_crop_path:
                    print(f"🖼️  Face crop saved: {face_crop_path}")
                    current_face_crop_path = face_crop_path  # Track for cleanup
            else:
                print("💡 Use --show-faces flag to display individual face crops")
                current_face_crop_path = None
            
            print(f"\n🖼️  Viewing face {current_pos}/{total_unidentified} from {filename}")
            
            # Clear and update image
            self._update_face_image(gui_components, show_faces, face_crop_path, photo_path)
            
            # Set person name input - restore saved name or use database/empty value
            self._restore_person_name_input(gui_components, face_id, face_person_names, is_already_identified)
            
            # Keep compare checkbox state persistent across navigation
            gui_components['first_name_entry'].focus_set()
            gui_components['first_name_entry'].icursor(0)
            
            # Force GUI update before waiting for input
            root.update_idletasks()
            
            # Wait for user input
            while waiting_for_input:
                try:
                    root.update()
                    # Check for command from GUI buttons
                    if gui_components['command_var'].get():
                        command = gui_components['command_var'].get()
                        gui_components['command_var'].set("")  # Clear the command
                        waiting_for_input = False
                        break
                    
                    # Check for unique checkbox changes
                    if hasattr(self, '_last_unique_state'):
                        current_unique_state = gui_components['unique_var'].get()
                        if current_unique_state != self._last_unique_state:
                            # Unique checkbox state changed, apply filtering
                            original_faces = self._on_unique_faces_change(
                                gui_components, original_faces, i, face_status,
                                date_from, date_to, date_processed_from, date_processed_to
                            )
                            # Reset index to 0 when filtering changes
                            i = 0
                            self._last_unique_state = current_unique_state
                            # Continue to next iteration to update display
                            continue
                    else:
                        # Initialize the last unique state
                        self._last_unique_state = gui_components['unique_var'].get()
                    
                    # Check for compare checkbox changes
                    if hasattr(self, '_last_compare_state'):
                        current_compare_state = gui_components['compare_var'].get()
                        if current_compare_state != self._last_compare_state:
                            # Compare checkbox state changed, update similar faces panel
                            self._on_compare_change(
                                gui_components, face_id, tolerance, face_status,
                                face_selection_states, identify_data_cache, original_faces, i
                            )
                            self._last_compare_state = current_compare_state
                            # Continue to next iteration to update display
                            continue
                    else:
                        # Initialize the last compare state
                        self._last_compare_state = gui_components['compare_var'].get()
                    
                    # Small delay to prevent excessive CPU usage
                    time.sleep(0.01)
                except tk.TclError:
                    # Window was destroyed, break out of loop
                    break
                
                # Check if force exit was requested
                if force_exit:
                    break
            
            # Check if force exit was requested (exit immediately)
            if force_exit:
                print("Force exit requested...")
                # Clean up face crops and caches
                self.face_processor.cleanup_face_crops(face_crop_path)
                self.db.close_db_connection()
                return identified_count
            
            # Process the command
            if command is None:  # User clicked Cancel
                command = 'q'
            else:
                command = command.strip()
            
            if command.lower() == 'q':
                # Clean up face crops and caches
                self.face_processor.cleanup_face_crops(face_crop_path)
                self.db.close_db_connection()
                
                if not window_destroyed:
                    window_destroyed = True
                    try:
                        root.destroy()
                    except tk.TclError:
                        pass  # Window already destroyed
                return identified_count
            
            elif command.lower() == 's':
                print("➡️  Next")
                
                # Save current checkbox states before navigating away
                self._save_current_face_selection_states(gui_components, original_faces, i, 
                                                       face_selection_states, face_person_names)
                
                # Clean up current face crop when moving forward
                if face_crop_path and os.path.exists(face_crop_path):
                    try:
                        os.remove(face_crop_path)
                    except:
                        pass  # Ignore cleanup errors
                current_face_crop_path = None  # Clear tracked path
                
                # Find next unidentified face
                next_found = False
                for j in range(i + 1, len(original_faces)):
                    if original_faces[j][0] not in face_status or face_status[original_faces[j][0]] != 'identified':
                        i = j
                        next_found = True
                        break
                
                if not next_found:
                    print("⚠️  No more unidentified faces - Next button disabled")
                    continue
                
                # Clear date of birth field when moving to next face
                gui_components['date_of_birth_var'].set("")
                # Clear middle name and maiden name fields when moving to next face
                gui_components['middle_name_var'].set("")
                gui_components['maiden_name_var'].set("")
                
                self._update_button_states(gui_components, original_faces, i, face_status)
                # Only update similar faces if compare is enabled
                if gui_components['compare_var'].get():
                    self._update_similar_faces(gui_components, face_id, tolerance, face_status, 
                                             face_selection_states, identify_data_cache, original_faces, i)
                continue
            
            elif command.lower() == 'back':
                print("⬅️  Going back to previous face")
                
                # Save current checkbox states before navigating away
                self._save_current_face_selection_states(gui_components, original_faces, i, 
                                                       face_selection_states, face_person_names)
                
                # Find previous unidentified face
                prev_found = False
                for j in range(i - 1, -1, -1):
                    if original_faces[j][0] not in face_status or face_status[original_faces[j][0]] != 'identified':
                        i = j
                        prev_found = True
                        break
                
                if not prev_found:
                    print("⚠️  No more unidentified faces - Back button disabled")
                    continue
                
                # Repopulate fields with saved data when going back
                self._restore_person_name_input(gui_components, original_faces[i][0], face_person_names, False)
                
                self._update_button_states(gui_components, original_faces, i, face_status)
                # Only update similar faces if compare is enabled
                if gui_components['compare_var'].get():
                    self._update_similar_faces(gui_components, face_id, tolerance, face_status, 
                                             face_selection_states, identify_data_cache, original_faces, i)
                continue
            
            elif command == 'reload_faces':
                # Reload faces with new date filters
                if 'filtered_faces' in gui_components:
                    # Update the original_faces list with filtered results
                    original_faces = list(gui_components['filtered_faces'])
                    
                    # Update the date filter variables
                    date_from = gui_components.get('new_date_from')
                    date_to = gui_components.get('new_date_to')
                    date_processed_from = gui_components.get('new_date_processed_from')
                    date_processed_to = gui_components.get('new_date_processed_to')
                    
                    # Reset to first face
                    i = 0
                    
                    # Clear the filtered_faces data
                    del gui_components['filtered_faces']
                    
                    print("💡 Navigate to refresh the display with filtered faces")
                    continue
                else:
                    print("⚠️  No filtered faces data found")
                    continue
            
            elif command.lower() == 'list':
                self._show_people_list()
                continue
            
            elif command == 'identify':
                try:
                    # Get form data
                    form_data = self._get_form_data(gui_components)
                    
                    # Validate form data
                    is_valid, error_msg = self._validate_form_data(form_data)
                    if not is_valid:
                        messagebox.showerror("Validation Error", error_msg)
                        continue
                    
                    # Process identification
                    identified_count += self._process_identification_command(
                        form_data, face_id, is_already_identified, face_status, 
                        gui_components, identify_data_cache
                    )
                    
                    # Clear form after successful identification
                    self._clear_form(gui_components)
                    
                    # Update face index to move to next unidentified face
                    has_faces, i = self._update_current_face_index(original_faces, i, face_status)
                    if not has_faces:
                        # All faces have been identified
                        print("\n🎉 All faces have been identified!")
                        break
                    
                except Exception as e:
                    print(f"❌ Error: {e}")
                    messagebox.showerror("Error", f"Error processing identification: {e}")
                
                # Clean up current face crop when moving forward after identification
                if face_crop_path and os.path.exists(face_crop_path):
                    try:
                        os.remove(face_crop_path)
                    except:
                        pass  # Ignore cleanup errors
                current_face_crop_path = None  # Clear tracked path
                
                # Continue to next face after processing command
                continue
            
            elif command:
                try:
                    # Process other identification command (legacy support)
                    identified_count += self._process_identification_command(
                        command, face_id, is_already_identified, face_status, 
                        gui_components, identify_data_cache
                    )
                    
                except Exception as e:
                    print(f"❌ Error: {e}")
                
                # Increment index for normal flow (identification or error) - but not if we're at the last item
                if i < len(original_faces) - 1:
                    i += 1
                self._update_button_states(gui_components, original_faces, i, face_status)
                # Only update similar faces if compare is enabled
                if gui_components['compare_var'].get():
                    self._update_similar_faces(gui_components, face_id, tolerance, face_status, 
                                             face_selection_states, identify_data_cache, original_faces, i)
                
                # Clean up current face crop when moving forward after identification
                if face_crop_path and os.path.exists(face_crop_path):
                    try:
                        os.remove(face_crop_path)
                    except:
                        pass  # Ignore cleanup errors
                current_face_crop_path = None  # Clear tracked path
                
                # Continue to next face after processing command
                continue
            else:
                print("Please enter a name, 's' to skip, 'q' to quit, or use buttons")
        
        # Only close the window if user explicitly quit (not when reaching end of faces)
        if not window_destroyed:
            # Keep the window open - user can still navigate and quit manually
            print(f"\n✅ Identified {identified_count} faces")
            print("💡 Application remains open - use Quit button to close")
            # Don't destroy the window - let user quit manually
            return identified_count
        
        print(f"\n✅ Identified {identified_count} faces")
        return identified_count
    
    def _get_unidentified_faces(self, batch_size: int, date_from: str = None, date_to: str = None, 
                               date_processed_from: str = None, date_processed_to: str = None):
        """Get unidentified faces from database with optional date filtering"""
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            
            # Build the SQL query with optional date filtering
            query = '''
                SELECT f.id, f.photo_id, p.path, p.filename, f.location 
                FROM faces f 
                JOIN photos p ON f.photo_id = p.id 
                WHERE f.person_id IS NULL 
            '''
            params = []
            
            # Add date taken filtering if specified
            if date_from:
                query += ' AND p.date_taken >= ?'
                params.append(date_from)
            
            if date_to:
                query += ' AND p.date_taken <= ?'
                params.append(date_to)
            
            # Add date processed filtering if specified
            if date_processed_from:
                query += ' AND DATE(p.date_added) >= ?'
                params.append(date_processed_from)
            
            if date_processed_to:
                query += ' AND DATE(p.date_added) <= ?'
                params.append(date_processed_to)
            
            query += ' LIMIT ?'
            params.append(batch_size)
            
            cursor.execute(query, params)
            return cursor.fetchall()
    
    def _prefetch_identify_data(self, unidentified):
        """Pre-fetch all needed data to avoid repeated database queries"""
        identify_data_cache = {}
        
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            
            # Pre-fetch all photo paths for unidentified faces
            photo_ids = [face[1] for face in unidentified]  # face[1] is photo_id
            if photo_ids:
                placeholders = ','.join('?' * len(photo_ids))
                cursor.execute(f'SELECT id, path, filename FROM photos WHERE id IN ({placeholders})', photo_ids)
                identify_data_cache['photo_paths'] = {row[0]: {'path': row[1], 'filename': row[2]} for row in cursor.fetchall()}
            
            # Pre-fetch all people names for dropdown
            cursor.execute('SELECT first_name, last_name, date_of_birth FROM people ORDER BY first_name, last_name')
            people = cursor.fetchall()
            identify_data_cache['people_names'] = [f"{first} {last}".strip() for first, last, dob in people]
            # Pre-fetch unique last names for autocomplete (no DB during typing)
            cursor.execute('SELECT DISTINCT last_name FROM people WHERE last_name IS NOT NULL AND TRIM(last_name) <> ""')
            _last_rows = cursor.fetchall()
            identify_data_cache['last_names'] = sorted({r[0].strip() for r in _last_rows if r and isinstance(r[0], str) and r[0].strip()})
        
        return identify_data_cache
    
    def _create_gui_components(self, main_frame, identify_data_cache, date_from, date_to, 
                              date_processed_from, date_processed_to, batch_size, on_quit=None):
        """Create all GUI components for the identify interface"""
        components = {}
        
        # Create variables for form data
        components['compare_var'] = tk.BooleanVar()
        components['unique_var'] = tk.BooleanVar()
        components['first_name_var'] = tk.StringVar()
        components['last_name_var'] = tk.StringVar()
        components['middle_name_var'] = tk.StringVar()
        components['maiden_name_var'] = tk.StringVar()
        components['date_of_birth_var'] = tk.StringVar()
        
        # Date filter variables
        components['date_from_var'] = tk.StringVar(value=date_from or "")
        components['date_to_var'] = tk.StringVar(value=date_to or "")
        components['date_processed_from_var'] = tk.StringVar(value=date_processed_from or "")
        components['date_processed_to_var'] = tk.StringVar(value=date_processed_to or "")
        
        # Date filter controls - exactly as in original
        date_filter_frame = ttk.LabelFrame(main_frame, text="Filter", padding="5")
        date_filter_frame.grid(row=1, column=0, pady=(0, 0), sticky=tk.W)
        date_filter_frame.columnconfigure(1, weight=0)
        date_filter_frame.columnconfigure(4, weight=0)
        
        # Date from
        ttk.Label(date_filter_frame, text="Taken date: from").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
        components['date_from_entry'] = ttk.Entry(date_filter_frame, textvariable=components['date_from_var'], width=10, state='readonly')
        components['date_from_entry'].grid(row=0, column=1, sticky=tk.W, padx=(0, 5))
        
        # Calendar button for date from
        def open_calendar_from():
            self._open_date_picker(components['date_from_var'])
        
        components['date_from_btn'] = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_from)
        components['date_from_btn'].grid(row=0, column=2, padx=(0, 10))
        
        # Date to
        ttk.Label(date_filter_frame, text="to").grid(row=0, column=3, sticky=tk.W, padx=(0, 5))
        components['date_to_entry'] = ttk.Entry(date_filter_frame, textvariable=components['date_to_var'], width=10, state='readonly')
        components['date_to_entry'].grid(row=0, column=4, sticky=tk.W, padx=(0, 5))
        
        # Calendar button for date to
        def open_calendar_to():
            self._open_date_picker(components['date_to_var'])
        
        components['date_to_btn'] = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_to)
        components['date_to_btn'].grid(row=0, column=5, padx=(0, 10))
        
        # Apply filter button
        def apply_date_filter():
            """Apply date filters and reload faces"""
            # Get current filter values
            new_date_from = components['date_from_var'].get().strip() or None
            new_date_to = components['date_to_var'].get().strip() or None
            new_date_processed_from = components['date_processed_from_var'].get().strip() or None
            new_date_processed_to = components['date_processed_to_var'].get().strip() or None
            
            # Reload faces with new date filter
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                
                # Build the SQL query with optional date filtering
                query = '''
                    SELECT f.id, f.photo_id, p.path, p.filename, f.location 
                    FROM faces f 
                    JOIN photos p ON f.photo_id = p.id 
                    WHERE f.person_id IS NULL 
                '''
                params = []
                
                # Add date taken filtering if specified
                if new_date_from:
                    query += ' AND p.date_taken >= ?'
                    params.append(new_date_from)
                
                if new_date_to:
                    query += ' AND p.date_taken <= ?'
                    params.append(new_date_to)
                
                # Add date processed filtering if specified
                if new_date_processed_from:
                    query += ' AND DATE(p.date_added) >= ?'
                    params.append(new_date_processed_from)
                
                if new_date_processed_to:
                    query += ' AND DATE(p.date_added) <= ?'
                    params.append(new_date_processed_to)
                
                query += ' LIMIT ?'
                params.append(batch_size)
                
                cursor.execute(query, params)
                filtered_faces = cursor.fetchall()
            
            if not filtered_faces:
                messagebox.showinfo("No Faces Found", "No unidentified faces found with the current date filters.")
                return
            
            # Build filter description
            filters_applied = []
            if new_date_from or new_date_to:
                taken_filter = f"taken: {new_date_from or 'any'} to {new_date_to or 'any'}"
                filters_applied.append(taken_filter)
            if new_date_processed_from or new_date_processed_to:
                processed_filter = f"processed: {new_date_processed_from or 'any'} to {new_date_processed_to or 'any'}"
                filters_applied.append(processed_filter)
            
            filter_desc = " | ".join(filters_applied) if filters_applied else "no filters"
            
            print(f"📅 Applied filters: {filter_desc}")
            print(f"👤 Found {len(filtered_faces)} unidentified faces with date filters")
            
            # Set a special command to reload faces
            components['command_var'].set("reload_faces")
            
            # Store the filtered faces for the main loop to use
            components['filtered_faces'] = filtered_faces
            components['new_date_from'] = new_date_from
            components['new_date_to'] = new_date_to
            components['new_date_processed_from'] = new_date_processed_from
            components['new_date_processed_to'] = new_date_processed_to
        
        components['apply_filter_btn'] = ttk.Button(date_filter_frame, text="Apply Filter", command=apply_date_filter)
        components['apply_filter_btn'].grid(row=0, column=6, padx=(10, 0))
        
        # Date processed filter (second row)
        ttk.Label(date_filter_frame, text="Processed date: from").grid(row=1, column=0, sticky=tk.W, padx=(0, 5), pady=(10, 0))
        components['date_processed_from_entry'] = ttk.Entry(date_filter_frame, textvariable=components['date_processed_from_var'], width=10, state='readonly')
        components['date_processed_from_entry'].grid(row=1, column=1, sticky=tk.W, padx=(0, 5), pady=(10, 0))
        
        # Calendar button for date processed from
        def open_calendar_processed_from():
            self._open_date_picker(components['date_processed_from_var'])
        
        components['date_processed_from_btn'] = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_processed_from)
        components['date_processed_from_btn'].grid(row=1, column=2, padx=(0, 10), pady=(10, 0))
        
        # Date processed to
        ttk.Label(date_filter_frame, text="to").grid(row=1, column=3, sticky=tk.W, padx=(0, 5), pady=(10, 0))
        components['date_processed_to_entry'] = ttk.Entry(date_filter_frame, textvariable=components['date_processed_to_var'], width=10, state='readonly')
        components['date_processed_to_entry'].grid(row=1, column=4, sticky=tk.W, padx=(0, 5), pady=(10, 0))
        
        # Calendar button for date processed to
        def open_calendar_processed_to():
            self._open_date_picker(components['date_processed_to_var'])
        
        components['date_processed_to_btn'] = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_processed_to)
        components['date_processed_to_btn'].grid(row=1, column=5, padx=(0, 10), pady=(10, 0))
        
        # Unique checkbox under the filter frame
        def on_unique_change():
            # This will be called when the checkbox state changes
            # We'll handle the actual filtering in the main loop
            pass
        
        components['unique_check'] = ttk.Checkbutton(main_frame, text="Unique faces only", 
                                                   variable=components['unique_var'],
                                                   command=on_unique_change)
        components['unique_check'].grid(row=2, column=0, sticky=tk.W, padx=(0, 5), pady=0)
        
        # Compare checkbox on the same row as Unique
        def on_compare_change():
            # This will be called when the checkbox state changes
            # We'll handle the actual panel toggling in the main loop
            pass
        
        components['compare_check'] = ttk.Checkbutton(main_frame, text="Compare similar faces", 
                                                    variable=components['compare_var'],
                                                    command=on_compare_change)
        components['compare_check'].grid(row=2, column=1, sticky=tk.W, padx=(5, 10), pady=0)
        
        # Left panel for main face
        left_panel = ttk.Frame(main_frame)
        left_panel.grid(row=3, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5), pady=(0, 0))
        left_panel.columnconfigure(0, weight=1)
        
        # Right panel for similar faces
        components['right_panel'] = ttk.LabelFrame(main_frame, text="Similar Faces", padding="5")
        components['right_panel'].grid(row=3, column=1, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
        components['right_panel'].columnconfigure(0, weight=1)
        components['right_panel'].rowconfigure(0, weight=1)  # Make right panel expandable vertically
        
        # Right panel is always visible now
        
        # Image display (left panel)
        image_frame = ttk.Frame(left_panel)
        image_frame.grid(row=0, column=0, pady=(0, 10), sticky=(tk.W, tk.E, tk.N, tk.S))
        image_frame.columnconfigure(0, weight=1)
        image_frame.rowconfigure(0, weight=1)
        
        # Create canvas for image display
        style = ttk.Style()
        canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
        components['canvas'] = tk.Canvas(image_frame, width=400, height=400, bg=canvas_bg_color, highlightthickness=0)
        components['canvas'].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Store reference to current image data for redrawing on resize
        components['canvas'].current_image_data = None
        
        # Bind resize event to redraw image
        def on_canvas_resize(event):
            if hasattr(components['canvas'], 'current_image_data') and components['canvas'].current_image_data:
                # Redraw the current image with new dimensions
                self._redraw_current_image(components['canvas'])
        
        components['canvas'].bind('<Configure>', on_canvas_resize)
        
        # Input section (left panel)
        input_frame = ttk.LabelFrame(left_panel, text="Person Identification", padding="10")
        input_frame.grid(row=1, column=0, pady=(10, 0), sticky=(tk.W, tk.E))
        input_frame.columnconfigure(1, weight=1)
        input_frame.columnconfigure(3, weight=1)
        input_frame.columnconfigure(5, weight=1)
        input_frame.columnconfigure(7, weight=1)
        
        # First name input
        ttk.Label(input_frame, text="First name:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10))
        components['first_name_entry'] = ttk.Entry(input_frame, textvariable=components['first_name_var'], width=12)
        components['first_name_entry'].grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 5))
        
        # Last name input with autocomplete
        ttk.Label(input_frame, text="Last name:").grid(row=0, column=2, sticky=tk.W, padx=(10, 10))
        components['last_name_entry'] = ttk.Entry(input_frame, textvariable=components['last_name_var'], width=12)
        components['last_name_entry'].grid(row=0, column=3, sticky=(tk.W, tk.E), padx=(0, 5))
        
        # Middle name input
        ttk.Label(input_frame, text="Middle name:").grid(row=0, column=4, sticky=tk.W, padx=(10, 10))
        components['middle_name_entry'] = ttk.Entry(input_frame, textvariable=components['middle_name_var'], width=12)
        components['middle_name_entry'].grid(row=0, column=5, sticky=(tk.W, tk.E), padx=(0, 5))
        
        # Date of birth input with calendar chooser
        ttk.Label(input_frame, text="Date of birth:").grid(row=1, column=0, sticky=tk.W, padx=(0, 10))
        
        # Create a frame for the date picker
        date_frame = ttk.Frame(input_frame)
        date_frame.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
        
        # Maiden name input
        ttk.Label(input_frame, text="Maiden name:").grid(row=1, column=2, sticky=tk.W, padx=(10, 10))
        components['maiden_name_entry'] = ttk.Entry(input_frame, textvariable=components['maiden_name_var'], width=12)
        components['maiden_name_entry'].grid(row=1, column=3, sticky=(tk.W, tk.E), padx=(0, 5))
        
        # Date display entry (read-only)
        components['date_of_birth_entry'] = ttk.Entry(date_frame, textvariable=components['date_of_birth_var'], width=12, state='readonly')
        components['date_of_birth_entry'].pack(side=tk.LEFT, fill=tk.X, expand=True)
        
        # Calendar button
        components['date_of_birth_btn'] = ttk.Button(date_frame, text="📅", width=3, 
                                                   command=lambda: self._open_date_picker(components['date_of_birth_var']))
        components['date_of_birth_btn'].pack(side=tk.RIGHT, padx=(15, 0))
        
        # Add required field asterisks (like in original)
        self._add_required_asterisks(main_frame.master, input_frame, components)
        
        # Add autocomplete for last name (like in original)
        self._setup_last_name_autocomplete(main_frame.master, components, identify_data_cache)
        
        # Identify button (placed in Person Identification frame)
        components['identify_btn'] = ttk.Button(input_frame, text="✅ Identify", command=lambda: self._set_command('identify'), state='disabled')
        components['identify_btn'].grid(row=2, column=0, pady=(10, 0), sticky=tk.W)
        
        # Add event handlers to update Identify button state
        def update_identify_button_state(*args):
            self._update_identify_button_state(components)
        
        components['first_name_var'].trace('w', update_identify_button_state)
        components['last_name_var'].trace('w', update_identify_button_state)
        components['date_of_birth_var'].trace('w', update_identify_button_state)
        
        # Handle Enter key
        def on_enter(event):
            if components['identify_btn']['state'] == 'normal':
                self._set_command('identify')
        
        components['first_name_entry'].bind('<Return>', on_enter)
        components['last_name_entry'].bind('<Return>', on_enter)
        components['middle_name_entry'].bind('<Return>', on_enter)
        components['maiden_name_entry'].bind('<Return>', on_enter)
        
        
        # Create similar faces frame with controls
        similar_faces_frame = ttk.Frame(components['right_panel'])
        similar_faces_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        similar_faces_frame.columnconfigure(0, weight=1)
        similar_faces_frame.rowconfigure(0, weight=0)  # Controls row takes minimal space
        similar_faces_frame.rowconfigure(1, weight=1)  # Canvas row expandable
        
        # Control buttons for similar faces (Select All / Clear All)
        similar_controls_frame = ttk.Frame(similar_faces_frame)
        similar_controls_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=5, pady=(5, 0))
        
        def select_all_similar_faces():
            """Select all similar faces checkboxes"""
            if hasattr(self, '_similar_face_vars'):
                for face_id, var in self._similar_face_vars:
                    var.set(True)
        
        def clear_all_similar_faces():
            """Clear all similar faces checkboxes"""
            if hasattr(self, '_similar_face_vars'):
                for face_id, var in self._similar_face_vars:
                    var.set(False)
        
        components['select_all_btn'] = ttk.Button(similar_controls_frame, text="☑️ Select All", 
                                                command=select_all_similar_faces, state='disabled')
        components['select_all_btn'].pack(side=tk.LEFT, padx=(0, 5))
        
        components['clear_all_btn'] = ttk.Button(similar_controls_frame, text="☐ Clear All", 
                                               command=clear_all_similar_faces, state='disabled')
        components['clear_all_btn'].pack(side=tk.LEFT)
        
        # Create canvas for similar faces with scrollbar
        similar_canvas = tk.Canvas(similar_faces_frame, bg='lightgray', relief='sunken', bd=2)
        similar_canvas.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Similar faces scrollbars
        similar_v_scrollbar = ttk.Scrollbar(similar_faces_frame, orient='vertical', command=similar_canvas.yview)
        similar_v_scrollbar.grid(row=1, column=1, sticky=(tk.N, tk.S))
        similar_canvas.configure(yscrollcommand=similar_v_scrollbar.set)
        
        # Create scrollable frame for similar faces
        components['similar_scrollable_frame'] = ttk.Frame(similar_canvas)
        similar_canvas.create_window((0, 0), window=components['similar_scrollable_frame'], anchor='nw')
        
        # Store canvas reference for scrolling
        components['similar_canvas'] = similar_canvas
        
        # Add initial message when compare is disabled
        no_compare_label = ttk.Label(components['similar_scrollable_frame'], text="Enable 'Compare similar faces' to see similar faces", 
                                   foreground="gray", font=("Arial", 10))
        no_compare_label.pack(pady=20)
        
        # Bottom control panel (move to bottom below panels)
        control_frame = ttk.Frame(main_frame)
        control_frame.grid(row=4, column=0, columnspan=3, pady=(10, 0), sticky=(tk.E, tk.S))
        
        # Create button references for state management
        components['control_back_btn'] = ttk.Button(control_frame, text="⬅️ Back", command=lambda: self._set_command('back'))
        components['control_next_btn'] = ttk.Button(control_frame, text="➡️ Next", command=lambda: self._set_command('s'))
        if on_quit:
            components['control_quit_btn'] = ttk.Button(control_frame, text="❌ Quit", command=on_quit)
        else:
            components['control_quit_btn'] = ttk.Button(control_frame, text="❌ Quit", command=lambda: self._set_command('quit'))
        
        components['control_back_btn'].pack(side=tk.LEFT, padx=(0, 5))
        components['control_next_btn'].pack(side=tk.LEFT, padx=(0, 5))
        components['control_quit_btn'].pack(side=tk.LEFT, padx=(5, 0))
        
        # Store command variable for button callbacks
        components['command_var'] = tk.StringVar()
        
        return components
    
    def _add_required_asterisks(self, root, input_frame, components):
        """Add red asterisks to required fields (first name, last name, date of birth)"""
        # Red asterisks for required fields (overlayed, no layout impact)
        first_name_asterisk = ttk.Label(root, text="*", foreground="red")
        first_name_asterisk.place_forget()
        
        last_name_asterisk = ttk.Label(root, text="*", foreground="red")
        last_name_asterisk.place_forget()
        
        date_asterisk = ttk.Label(root, text="*", foreground="red")
        date_asterisk.place_forget()
        
        def _position_required_asterisks(event=None):
            """Position required asterisks at top-right corner of their entries."""
            try:
                root.update_idletasks()
                input_frame.update_idletasks()
                components['first_name_entry'].update_idletasks()
                components['last_name_entry'].update_idletasks()
                components['date_of_birth_entry'].update_idletasks()
                
                # Get absolute coordinates relative to root window
                first_root_x = components['first_name_entry'].winfo_rootx()
                first_root_y = components['first_name_entry'].winfo_rooty()
                first_w = components['first_name_entry'].winfo_width()
                root_x = root.winfo_rootx()
                root_y = root.winfo_rooty()
                
                # First name asterisk at the true top-right corner of entry
                first_name_asterisk.place(x=first_root_x - root_x + first_w + 2, y=first_root_y - root_y - 2, anchor='nw')
                first_name_asterisk.lift()
                
                # Last name asterisk at the true top-right corner of entry
                last_root_x = components['last_name_entry'].winfo_rootx()
                last_root_y = components['last_name_entry'].winfo_rooty()
                last_w = components['last_name_entry'].winfo_width()
                last_name_asterisk.place(x=last_root_x - root_x + last_w + 2, y=last_root_y - root_y - 2, anchor='nw')
                last_name_asterisk.lift()

                # Date of birth asterisk at the true top-right corner of date entry
                dob_root_x = components['date_of_birth_entry'].winfo_rootx()
                dob_root_y = components['date_of_birth_entry'].winfo_rooty()
                dob_w = components['date_of_birth_entry'].winfo_width()
                date_asterisk.place(x=dob_root_x - root_x + dob_w + 2, y=dob_root_y - root_y - 2, anchor='nw')
                date_asterisk.lift()
            except Exception:
                pass

        # Bind repositioning after all entries are created
        def _bind_asterisk_positioning():
            try:
                input_frame.bind('<Configure>', _position_required_asterisks)
                components['first_name_entry'].bind('<Configure>', _position_required_asterisks)
                components['last_name_entry'].bind('<Configure>', _position_required_asterisks)
                components['date_of_birth_entry'].bind('<Configure>', _position_required_asterisks)
                _position_required_asterisks()
            except Exception:
                pass
        root.after(100, _bind_asterisk_positioning)
    
    def _setup_last_name_autocomplete(self, root, components, identify_data_cache):
        """Setup autocomplete functionality for last name field - exact copy from original"""
        # Create listbox for suggestions (as overlay attached to root, not clipped by frames)
        last_name_listbox = tk.Listbox(root, height=8)
        last_name_listbox.place_forget()  # Hide initially
        
        # Navigation state variables (like in original)
        navigating_to_listbox = False
        escape_pressed = False
        enter_pressed = False
        
        def _show_suggestions():
            """Show filtered suggestions in listbox"""
            all_last_names = identify_data_cache.get('last_names', [])
            typed = components['last_name_var'].get().strip()
            
            if not typed:
                filtered = []  # Show nothing if no typing
            else:
                low = typed.lower()
                # Only show names that start with the typed text
                filtered = [n for n in all_last_names if n.lower().startswith(low)][:10]
            
            # Update listbox
            last_name_listbox.delete(0, tk.END)
            for name in filtered:
                last_name_listbox.insert(tk.END, name)
            
            # Show listbox if we have suggestions (as overlay)
            if filtered:
                # Ensure geometry is up to date before positioning
                root.update_idletasks()
                # Absolute coordinates of entry relative to screen
                entry_root_x = components['last_name_entry'].winfo_rootx()
                entry_root_y = components['last_name_entry'].winfo_rooty()
                entry_height = components['last_name_entry'].winfo_height()
                # Convert to coordinates relative to root
                root_origin_x = root.winfo_rootx()
                root_origin_y = root.winfo_rooty()
                place_x = entry_root_x - root_origin_x
                place_y = entry_root_y - root_origin_y + entry_height
                place_width = components['last_name_entry'].winfo_width()
                # Calculate how many rows fit to bottom of window
                available_px = max(60, root.winfo_height() - place_y - 8)
                # Approximate row height in pixels; 18 is a reasonable default for Tk listbox rows
                approx_row_px = 18
                rows_fit = max(3, min(len(filtered), available_px // approx_row_px))
                last_name_listbox.configure(height=rows_fit)
                last_name_listbox.place(x=place_x, y=place_y, width=place_width)
                last_name_listbox.selection_clear(0, tk.END)
                last_name_listbox.selection_set(0)  # Select first item
                last_name_listbox.activate(0)  # Activate first item
            else:
                last_name_listbox.place_forget()
        
        def _hide_suggestions():
            """Hide the suggestions listbox"""
            last_name_listbox.place_forget()
        
        def _on_listbox_select(event=None):
            """Handle listbox selection and hide list"""
            selection = last_name_listbox.curselection()
            if selection:
                selected_name = last_name_listbox.get(selection[0])
                components['last_name_var'].set(selected_name)
            _hide_suggestions()
            components['last_name_entry'].focus_set()

        def _on_listbox_click(event):
            """Handle mouse click selection"""
            try:
                index = last_name_listbox.nearest(event.y)
                if index is not None and index >= 0:
                    selected_name = last_name_listbox.get(index)
                    components['last_name_var'].set(selected_name)
            except:
                pass
            _hide_suggestions()
            components['last_name_entry'].focus_set()
            return 'break'
        
        def _on_key_press(event):
            """Handle key navigation in entry"""
            nonlocal navigating_to_listbox, escape_pressed, enter_pressed
            if event.keysym == 'Down':
                if last_name_listbox.winfo_ismapped():
                    navigating_to_listbox = True
                    last_name_listbox.focus_set()
                    last_name_listbox.selection_clear(0, tk.END)
                    last_name_listbox.selection_set(0)
                    last_name_listbox.activate(0)
                return 'break'
            elif event.keysym == 'Escape':
                escape_pressed = True
                _hide_suggestions()
                return 'break'
            elif event.keysym == 'Return':
                enter_pressed = True
                return 'break'
        
        def _on_listbox_key(event):
            """Handle key navigation in listbox"""
            nonlocal enter_pressed, escape_pressed
            if event.keysym == 'Return':
                enter_pressed = True
                _on_listbox_select(event)
                return 'break'
            elif event.keysym == 'Escape':
                escape_pressed = True
                _hide_suggestions()
                components['last_name_entry'].focus_set()
                return 'break'
            elif event.keysym == 'Up':
                selection = last_name_listbox.curselection()
                if selection and selection[0] > 0:
                    # Move up in listbox
                    last_name_listbox.selection_clear(0, tk.END)
                    last_name_listbox.selection_set(selection[0] - 1)
                    last_name_listbox.see(selection[0] - 1)
                else:
                    # At top, go back to entry field
                    _hide_suggestions()
                    components['last_name_entry'].focus_set()
                return 'break'
            elif event.keysym == 'Down':
                selection = last_name_listbox.curselection()
                max_index = last_name_listbox.size() - 1
                if selection and selection[0] < max_index:
                    # Move down in listbox
                    last_name_listbox.selection_clear(0, tk.END)
                    last_name_listbox.selection_set(selection[0] + 1)
                    last_name_listbox.see(selection[0] + 1)
                return 'break'
        
        # Track if we're navigating to listbox to prevent auto-hide
        navigating_to_listbox = False
        escape_pressed = False
        enter_pressed = False
        
        def _safe_hide_suggestions():
            """Hide suggestions only if not navigating to listbox"""
            nonlocal navigating_to_listbox
            if not navigating_to_listbox:
                _hide_suggestions()
            navigating_to_listbox = False
        
        def _safe_show_suggestions():
            """Show suggestions only if escape or enter wasn't just pressed"""
            nonlocal escape_pressed, enter_pressed
            if not escape_pressed and not enter_pressed:
                _show_suggestions()
            escape_pressed = False
            enter_pressed = False
        
        # Bind events
        components['last_name_entry'].bind('<KeyRelease>', lambda e: _safe_show_suggestions())
        components['last_name_entry'].bind('<KeyPress>', _on_key_press)
        components['last_name_entry'].bind('<FocusOut>', lambda e: root.after(150, _safe_hide_suggestions))  # Delay to allow listbox clicks
        last_name_listbox.bind('<Button-1>', _on_listbox_click)
        last_name_listbox.bind('<KeyPress>', _on_listbox_key)
        last_name_listbox.bind('<Double-Button-1>', _on_listbox_click)
    
    def _update_identify_button_state(self, gui_components):
        """Update the state of the Identify button based on form data"""
        first_name = gui_components['first_name_var'].get().strip()
        last_name = gui_components['last_name_var'].get().strip()
        date_of_birth = gui_components['date_of_birth_var'].get().strip()
        
        # Enable button if we have at least first name or last name, and date of birth
        if (first_name or last_name) and date_of_birth:
            gui_components['identify_btn'].config(state='normal')
        else:
            gui_components['identify_btn'].config(state='disabled')
    
    def _update_control_button_states(self, gui_components, i, total_faces):
        """Update the state of control buttons based on current position"""
        # Back button - disabled if at first face
        if i <= 0:
            gui_components['control_back_btn'].config(state='disabled')
        else:
            gui_components['control_back_btn'].config(state='normal')
        
        # Next button - disabled if at last face
        if i >= total_faces - 1:
            gui_components['control_next_btn'].config(state='disabled')
        else:
            gui_components['control_next_btn'].config(state='normal')
    
    def _update_select_clear_buttons_state(self, gui_components, similar_face_vars):
        """Enable/disable Select All and Clear All based on compare state and presence of items"""
        if gui_components['compare_var'].get() and similar_face_vars:
            gui_components['select_all_btn'].config(state='normal')
            gui_components['clear_all_btn'].config(state='normal')
        else:
            gui_components['select_all_btn'].config(state='disabled')
            gui_components['clear_all_btn'].config(state='disabled')
    
    def _get_pending_identifications(self, face_person_names, face_status):
        """Get pending identifications that haven't been saved yet"""
        pending_identifications = {}
        for k, v in face_person_names.items():
            if k not in face_status or face_status[k] != 'identified':
                # Handle person data dict format
                if isinstance(v, dict):
                    first_name = v.get('first_name', '').strip()
                    last_name = v.get('last_name', '').strip()
                    date_of_birth = v.get('date_of_birth', '').strip()
                    
                    # Check if we have complete data (both first and last name, plus date of birth)
                    if first_name and last_name and date_of_birth:
                        pending_identifications[k] = v
        return pending_identifications
    
    def _save_all_pending_identifications(self, face_person_names, face_status, identify_data_cache):
        """Save all pending identifications from face_person_names"""
        saved_count = 0
        
        for face_id, person_data in face_person_names.items():
            # Handle person data dict format
            if isinstance(person_data, dict):
                first_name = person_data.get('first_name', '').strip()
                last_name = person_data.get('last_name', '').strip()
                date_of_birth = person_data.get('date_of_birth', '').strip()
                middle_name = person_data.get('middle_name', '').strip()
                maiden_name = person_data.get('maiden_name', '').strip()
                
                # Only save if we have at least a first or last name
                if first_name or last_name:
                    try:
                        with self.db.get_db_connection() as conn:
                            cursor = conn.cursor()
                            # Add person if doesn't exist
                            cursor.execute('INSERT OR IGNORE INTO people (first_name, last_name, middle_name, maiden_name, date_of_birth) VALUES (?, ?, ?, ?, ?)', (first_name, last_name, middle_name, maiden_name, date_of_birth))
                            cursor.execute('SELECT id FROM people WHERE first_name = ? AND last_name = ? AND middle_name = ? AND maiden_name = ? AND date_of_birth = ?', (first_name, last_name, middle_name, maiden_name, date_of_birth))
                            result = cursor.fetchone()
                            person_id = result[0] if result else None
                        
                            # Update people cache if new person was added
                            display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
                            if display_name not in identify_data_cache['people_names']:
                                identify_data_cache['people_names'].append(display_name)
                                identify_data_cache['people_names'].sort()  # Keep sorted
                            # Keep last names cache updated in-session
                            if last_name:
                                if 'last_names' not in identify_data_cache:
                                    identify_data_cache['last_names'] = []
                                if last_name not in identify_data_cache['last_names']:
                                    identify_data_cache['last_names'].append(last_name)
                                    identify_data_cache['last_names'].sort()
                            
                            # Assign face to person
                            cursor.execute(
                                'UPDATE faces SET person_id = ? WHERE id = ?',
                                (person_id, face_id)
                            )
                        
                        # Update person encodings
                        self.face_processor.update_person_encodings(person_id)
                        saved_count += 1
                        display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
                        print(f"✅ Saved identification: {display_name}")
                        
                    except Exception as e:
                        display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
                        print(f"❌ Error saving identification for {display_name}: {e}")
        
        if saved_count > 0:
            print(f"💾 Saved {saved_count} pending identifications")
        
        return saved_count
    
    def _update_current_face_index(self, original_faces, i, face_status):
        """Update the current face index to point to a valid unidentified face"""
        unidentified_faces = [face for face in original_faces if face[0] not in face_status or face_status[face[0]] != 'identified']
        if not unidentified_faces:
            # All faces identified, we're done
            return False, i
        
        # Find the current face in the unidentified list
        current_face_id = original_faces[i][0] if i < len(original_faces) else None
        if current_face_id and current_face_id in face_status and face_status[current_face_id] == 'identified':
            # Current face was just identified, find the next unidentified face
            if i < len(original_faces) - 1:
                # Try to find the next unidentified face
                for j in range(i + 1, len(original_faces)):
                    if original_faces[j][0] not in face_status or face_status[original_faces[j][0]] != 'identified':
                        i = j
                        break
                else:
                    # No more faces after current, go to previous
                    for j in range(i - 1, -1, -1):
                        if original_faces[j][0] not in face_status or face_status[original_faces[j][0]] != 'identified':
                            i = j
                            break
            else:
                # At the end, go to previous unidentified face
                for j in range(i - 1, -1, -1):
                    if original_faces[j][0] not in face_status or face_status[original_faces[j][0]] != 'identified':
                        i = j
                        break
        
        # Ensure index is within bounds
        if i >= len(original_faces):
            i = len(original_faces) - 1
        if i < 0:
            i = 0
            
        return True, i
    
    def _get_current_face_position(self, original_faces, i, face_status):
        """Get current face position among unidentified faces"""
        unidentified_faces = [face for face in original_faces if face[0] not in face_status or face_status[face[0]] != 'identified']
        current_face_id = original_faces[i][0] if i < len(original_faces) else None
        
        # Find position of current face in unidentified list
        for pos, face in enumerate(unidentified_faces):
            if face[0] == current_face_id:
                return pos + 1, len(unidentified_faces)
        
        return 1, len(unidentified_faces)  # Fallback
    
    def _update_button_states(self, gui_components, original_faces, i, face_status):
        """Update button states based on current position and unidentified faces"""
        # Update control button states
        self._update_control_button_states(gui_components, i, len(original_faces))
        
        # Update identify button state
        self._update_identify_button_state(gui_components)
        
        # Update similar faces control buttons state
        # Get similar face variables if they exist
        similar_face_vars = getattr(self, '_similar_face_vars', [])
        self._update_select_clear_buttons_state(gui_components, similar_face_vars)
    
    def _update_similar_faces(self, gui_components, face_id, tolerance, face_status, 
                             face_selection_states, identify_data_cache, original_faces, i):
        """Update the similar faces panel when compare is enabled"""
        try:
            if not gui_components['compare_var'].get():
                return
            
            scrollable_frame = gui_components['similar_scrollable_frame']
            
            # Clear existing content
            for widget in scrollable_frame.winfo_children():
                widget.destroy()
            
            # Get similar faces using filtered version (includes 40% confidence threshold)
            similar_faces = self.face_processor._get_filtered_similar_faces(face_id, tolerance, include_same_photo=False, face_status=face_status)
            
            if not similar_faces:
                no_faces_label = ttk.Label(scrollable_frame, text="No similar faces found", 
                                         foreground="gray", font=("Arial", 10))
                no_faces_label.pack(pady=20)
                return
            
            # Filter out already identified faces if unique checkbox is checked
            if gui_components['unique_var'].get():
                similar_faces = [face for face in similar_faces 
                               if face.get('person_id') is None]
            
            if not similar_faces:
                no_faces_label = ttk.Label(scrollable_frame, text="No unique similar faces found", 
                                         foreground="gray", font=("Arial", 10))
                no_faces_label.pack(pady=20)
                return
            
            # Sort by confidence (distance) - highest confidence first (lowest distance)
            similar_faces.sort(key=lambda x: x['distance'])
            
            # Display similar faces using the old version's approach
            self._display_similar_faces_in_panel(scrollable_frame, similar_faces, face_id, 
                                               face_selection_states, identify_data_cache)
            
            # Update canvas scroll region
            canvas = gui_components['similar_canvas']
            canvas.update_idletasks()
            canvas.configure(scrollregion=canvas.bbox("all"))
            
            if self.verbose >= 2:
                print(f"   🔍 Displayed {len(similar_faces)} similar faces")
                
        except Exception as e:
            print(f"❌ Error updating similar faces: {e}")
    
    def _display_similar_faces_in_panel(self, parent_frame, similar_faces_data, current_face_id, 
                                       face_selection_states, identify_data_cache):
        """Display similar faces in a panel - based on old version's auto-match display logic"""
        import tkinter as tk
        from tkinter import ttk
        from PIL import Image, ImageTk
        import os
        
        # Store similar face variables for Select All/Clear All functionality
        similar_face_vars = []
        
        # 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 = (1 - distance) * 100
            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()
            
            # 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=0)  # Image column - fixed width
            match_frame.columnconfigure(2, weight=1)  # Text column - expandable
            
            # 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))
            
            # Add to similar face variables list
            similar_face_vars.append((similar_face_id, match_var))
            
            # Right panel requirement: image immediately to the right of checkbox
            # Create canvas for face image next to checkbox
            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=1, rowspan=2, sticky=(tk.W, tk.N), padx=(5, 10))
            
            # Create labels container to the right of image
            info_container = ttk.Frame(match_frame)
            info_container.grid(row=0, column=2, rowspan=2, sticky=(tk.W, tk.E))
            
            # Confidence badge
            badge = self.gui_core.create_confidence_badge(info_container, confidence_pct)
            badge.pack(anchor=tk.W)
            
            filename_label = ttk.Label(info_container, text=f"📁 {filename}", font=("Arial", 8), foreground="gray")
            filename_label.pack(anchor=tk.W, pady=(2, 0))
            
            # Face image (reusing auto-match image display)
            try:
                # Get photo path from cache or database
                photo_path = None
                if identify_data_cache and 'photo_paths' in identify_data_cache:
                    # Find photo path by filename in cache
                    for photo_data in identify_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.face_processor._extract_face_crop(photo_path, face_data['location'], similar_face_id)
                if face_crop_path and os.path.exists(face_crop_path):
                    # Load and display image
                    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
                    
                    # Add photo icon exactly at the image's top-right corner
                    self.gui_core.create_photo_icon(match_canvas, photo_path, icon_size=15,
                                                  face_x=0, face_y=0, 
                                                  face_width=80, face_height=80,
                                                  canvas_width=80, canvas_height=80)
                    
                    # Clean up temporary face crop
                    try:
                        os.remove(face_crop_path)
                    except:
                        pass
                        
            except Exception as e:
                if self.verbose >= 1:
                    print(f"   ⚠️  Error displaying similar face {i}: {e}")
                continue
        
        # Store similar face variables for Select All/Clear All functionality
        self._similar_face_vars = similar_face_vars
    
    def _get_confidence_description(self, confidence_pct):
        """Get confidence description based on percentage"""
        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 _on_similar_face_select(self, similar_face_id, is_selected):
        """Handle similar face checkbox selection"""
        # This would be used to track which similar faces are selected
        # For now, just a placeholder
        pass
    
    def _get_person_name_for_face(self, face_id):
        """Get person name for a face that's already identified"""
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT p.first_name, p.last_name FROM people p 
                JOIN faces f ON p.id = f.person_id 
                WHERE f.id = ?
            ''', (face_id,))
            result = cursor.fetchone()
            if result:
                first_name, last_name = result
                if last_name and first_name:
                    return f"{last_name}, {first_name}"
                elif last_name:
                    return last_name
                elif first_name:
                    return first_name
                else:
                    return "Unknown"
            else:
                return "Unknown"
    
    def _update_face_image(self, gui_components, show_faces, face_crop_path, photo_path):
        """Update the face image display"""
        try:
            canvas = gui_components['canvas']
            
            # Clear existing image
            canvas.delete("all")
            
            # Determine which image to display
            if show_faces and face_crop_path and os.path.exists(face_crop_path):
                # Display face crop
                image_path = face_crop_path
                image_type = "face crop"
            elif photo_path and os.path.exists(photo_path):
                # Display full photo
                image_path = photo_path
                image_type = "full photo"
            else:
                # No image available
                canvas.create_text(200, 200, text="No image available", 
                                 font=("Arial", 12), fill="gray")
                return
            
            # Load and display image
            try:
                with Image.open(image_path) as img:
                    # Force canvas to update its dimensions
                    canvas.update_idletasks()
                    
                    # Get canvas dimensions - use configured size if not yet rendered
                    canvas_width = canvas.winfo_width()
                    canvas_height = canvas.winfo_height()
                    
                    # If canvas hasn't been rendered yet, use the configured size
                    if canvas_width <= 1 or canvas_height <= 1:
                        canvas_width = 400
                        canvas_height = 400
                        # Set a minimum size to ensure proper rendering
                        canvas.configure(width=canvas_width, height=canvas_height)
                    
                    # Get image dimensions
                    img_width, img_height = img.size
                    
                    # Calculate scale factor to fit image in canvas
                    scale_x = canvas_width / img_width
                    scale_y = canvas_height / img_height
                    scale = min(scale_x, scale_y, 1.0)  # Don't scale up
                    
                    # Calculate new dimensions
                    new_width = int(img_width * scale)
                    new_height = int(img_height * scale)
                    
                    # Resize image
                    img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
                    
                    # Convert to PhotoImage
                    photo = ImageTk.PhotoImage(img_resized)
                    
                    # Center image on canvas
                    x = (canvas_width - new_width) // 2
                    y = (canvas_height - new_height) // 2
                    
                    # Create image on canvas
                    canvas.create_image(x, y, anchor='nw', image=photo)
                    
                    # Keep reference to prevent garbage collection
                    canvas.image_ref = photo
                    
                    # Store image data for redrawing on resize
                    canvas.current_image_data = {
                        'image_path': image_path,
                        'image_type': image_type,
                        'original_img': img,
                        'img_width': img_width,
                        'img_height': img_height
                    }
                    
                    # Add photo icon using reusable function
                    self.gui_core.create_photo_icon(canvas, photo_path, 
                                                  face_x=x, face_y=y, 
                                                  face_width=new_width, face_height=new_height,
                                                  canvas_width=canvas_width, canvas_height=canvas_height)
                    
                    # Update canvas scroll region
                    canvas.configure(scrollregion=canvas.bbox("all"))
                    
                    if self.verbose >= 2:
                        print(f"   🖼️  Displayed {image_type}: {os.path.basename(image_path)} ({new_width}x{new_height})")
                        
            except Exception as e:
                canvas.create_text(200, 200, text=f"Error loading image:\n{str(e)}", 
                                 font=("Arial", 10), fill="red")
                if self.verbose >= 1:
                    print(f"   ⚠️  Error loading image {image_path}: {e}")
                    
        except Exception as e:
            print(f"❌ Error updating face image: {e}")
    
    def _redraw_current_image(self, canvas):
        """Redraw the current image when canvas is resized"""
        try:
            if not hasattr(canvas, 'current_image_data') or not canvas.current_image_data:
                return
            
            # Clear existing image
            canvas.delete("all")
            
            # Get stored image data
            data = canvas.current_image_data
            img = data['original_img']
            img_width = data['img_width']
            img_height = data['img_height']
            
            # Get current canvas dimensions
            canvas_width = canvas.winfo_width()
            canvas_height = canvas.winfo_height()
            
            if canvas_width <= 1 or canvas_height <= 1:
                return
            
            # Calculate new scale
            scale_x = canvas_width / img_width
            scale_y = canvas_height / img_height
            scale = min(scale_x, scale_y, 1.0)
            
            # Calculate new dimensions
            new_width = int(img_width * scale)
            new_height = int(img_height * scale)
            
            # Resize image
            img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
            
            # Convert to PhotoImage
            photo = ImageTk.PhotoImage(img_resized)
            
            # Center image on canvas
            x = (canvas_width - new_width) // 2
            y = (canvas_height - new_height) // 2
            
            # Create image on canvas
            canvas.create_image(x, y, anchor='nw', image=photo)
            
            # Keep reference to prevent garbage collection
            canvas.image_ref = photo
            
            # Add photo icon using reusable function
            self.gui_core.create_photo_icon(canvas, data['image_path'], 
                                          face_x=x, face_y=y, 
                                          face_width=new_width, face_height=new_height,
                                          canvas_width=canvas_width, canvas_height=canvas_height)
            
            # Update canvas scroll region
            canvas.configure(scrollregion=canvas.bbox("all"))
            
        except Exception as e:
            print(f"❌ Error redrawing image: {e}")
    
    def _restore_person_name_input(self, gui_components, face_id, face_person_names, is_already_identified):
        """Restore person name input fields"""
        try:
            if is_already_identified:
                # Get person data from database
                with self.db.get_db_connection() as conn:
                    cursor = conn.cursor()
                    cursor.execute('''
                        SELECT p.first_name, p.last_name, p.middle_name, p.maiden_name, p.date_of_birth 
                        FROM people p 
                        JOIN faces f ON p.id = f.person_id 
                        WHERE f.id = ?
                    ''', (face_id,))
                    result = cursor.fetchone()
                    
                    if result:
                        first_name, last_name, middle_name, maiden_name, date_of_birth = result
                        gui_components['first_name_var'].set(first_name or "")
                        gui_components['last_name_var'].set(last_name or "")
                        gui_components['middle_name_var'].set(middle_name or "")
                        gui_components['maiden_name_var'].set(maiden_name or "")
                        gui_components['date_of_birth_var'].set(date_of_birth or "")
                    else:
                        # Clear all fields if no person found
                        self._clear_form(gui_components)
            else:
                # Restore from saved data if available
                if face_id in face_person_names:
                    person_data = face_person_names[face_id]
                    if isinstance(person_data, dict):
                        gui_components['first_name_var'].set(person_data.get('first_name', ''))
                        gui_components['last_name_var'].set(person_data.get('last_name', ''))
                        gui_components['middle_name_var'].set(person_data.get('middle_name', ''))
                        gui_components['maiden_name_var'].set(person_data.get('maiden_name', ''))
                        gui_components['date_of_birth_var'].set(person_data.get('date_of_birth', ''))
                    else:
                        # Legacy string format
                        gui_components['first_name_var'].set(person_data or "")
                        gui_components['last_name_var'].set("")
                        gui_components['middle_name_var'].set("")
                        gui_components['maiden_name_var'].set("")
                        gui_components['date_of_birth_var'].set("")
                else:
                    # Clear all fields for new face
                    self._clear_form(gui_components)
                    
        except Exception as e:
            print(f"❌ Error restoring person name input: {e}")
            # Clear form on error
            self._clear_form(gui_components)
    
    def _save_current_face_selection_states(self, gui_components, original_faces, i, 
                                           face_selection_states, face_person_names):
        """Save current checkbox states and person name for the current face"""
        try:
            if i >= len(original_faces):
                return
                
            current_face_id = original_faces[i][0]
            
            # Save form data
            form_data = self._get_form_data(gui_components)
            face_person_names[current_face_id] = form_data
            
            # Save checkbox states for similar faces
            if current_face_id not in face_selection_states:
                face_selection_states[current_face_id] = {}
            
            # Note: Similar face checkbox states would be saved here
            # This would require tracking the checkbox variables created in _update_similar_faces
            
        except Exception as e:
            print(f"❌ Error saving face selection states: {e}")
    
    def _process_identification_command(self, command_or_data, face_id, is_already_identified, 
                                      face_status, gui_components, identify_data_cache):
        """Process an identification command"""
        try:
            # Handle form data (new GUI approach)
            if isinstance(command_or_data, dict):
                form_data = command_or_data
                first_name = form_data.get('first_name', '').strip()
                last_name = form_data.get('last_name', '').strip()
                middle_name = form_data.get('middle_name', '').strip()
                maiden_name = form_data.get('maiden_name', '').strip()
                date_of_birth = form_data.get('date_of_birth', '').strip()
            else:
                # Handle legacy string command
                command = command_or_data.strip()
                if not command:
                    return 0
                
                # Parse simple name format (legacy support)
                parts = command.split()
                if len(parts) >= 2:
                    first_name = parts[0]
                    last_name = ' '.join(parts[1:])
                else:
                    first_name = command
                    last_name = ""
                middle_name = ""
                maiden_name = ""
                date_of_birth = ""  # Legacy commands don't include date of birth
            
            # Add person if doesn't exist
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('INSERT OR IGNORE INTO people (first_name, last_name, middle_name, maiden_name, date_of_birth) VALUES (?, ?, ?, ?, ?)', 
                             (first_name, last_name, middle_name, maiden_name, date_of_birth))
                cursor.execute('SELECT id FROM people WHERE first_name = ? AND last_name = ? AND middle_name = ? AND maiden_name = ? AND date_of_birth = ?', 
                             (first_name, last_name, middle_name, maiden_name, date_of_birth))
                result = cursor.fetchone()
                person_id = result[0] if result else None
            
            # Update people cache if new person was added
            display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
            if display_name not in identify_data_cache['people_names']:
                identify_data_cache['people_names'].append(display_name)
                identify_data_cache['people_names'].sort()  # Keep sorted
            
            # Keep last names cache updated in-session
            if last_name:
                if 'last_names' not in identify_data_cache:
                    identify_data_cache['last_names'] = []
                if last_name not in identify_data_cache['last_names']:
                    identify_data_cache['last_names'].append(last_name)
                    identify_data_cache['last_names'].sort()
            
            # Get selected similar faces
            selected_similar_faces = self._get_selected_similar_faces()
            
            # Assign main face to person
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute(
                    'UPDATE faces SET person_id = ? WHERE id = ?',
                    (person_id, face_id)
                )
            
            # Assign selected similar faces to the same person
            similar_faces_identified = 0
            if selected_similar_faces:
                with self.db.get_db_connection() as conn:
                    cursor = conn.cursor()
                    for similar_face_id in selected_similar_faces:
                        cursor.execute(
                            'UPDATE faces SET person_id = ? WHERE id = ?',
                            (person_id, similar_face_id)
                        )
                        similar_faces_identified += 1
            
            # Update person encodings
            self.face_processor.update_person_encodings(person_id)
            
            # Mark main face as identified
            face_status[face_id] = 'identified'
            
            # Mark selected similar faces as identified
            for similar_face_id in selected_similar_faces:
                face_status[similar_face_id] = 'identified'
            
            display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
            if similar_faces_identified > 0:
                print(f"✅ Identified as: {display_name} (main face + {similar_faces_identified} similar faces)")
            else:
                print(f"✅ Identified as: {display_name}")
            
            return 1 + similar_faces_identified
            
        except Exception as e:
            print(f"❌ Error processing identification: {e}")
        return 0
    
    def _show_people_list(self):
        """Show list of known people"""
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('SELECT first_name, last_name FROM people ORDER BY first_name, last_name')
            people = cursor.fetchall()
            
        if people:
            formatted_names = [f"{last}, {first}".strip(", ").strip() for first, last in people if first or last]
            print("👥 Known people:", ", ".join(formatted_names))
        else:
            print("👥 No people identified yet")
    
    def _open_date_picker(self, date_var):
        """Open date picker dialog and update the date variable"""
        current_date = date_var.get()
        selected_date = self.gui_core.create_calendar_dialog(None, "Select Date", current_date)
        if selected_date is not None:
            date_var.set(selected_date)
    
    def _toggle_similar_faces_panel(self, components):
        """Update the similar faces panel content based on compare checkbox state"""
        # Panel is always visible now, just update content
        if not components['compare_var'].get():
            # Clear the similar faces content when compare is disabled
            scrollable_frame = components['similar_scrollable_frame']
            for widget in scrollable_frame.winfo_children():
                widget.destroy()
            
            # Show a message that compare is disabled
            no_compare_label = ttk.Label(scrollable_frame, text="Enable 'Compare similar faces' to see similar faces", 
                                       foreground="gray", font=("Arial", 10))
            no_compare_label.pack(pady=20)
    
    def _set_command(self, command):
        """Set the command variable to trigger the main loop"""
        # This will be used by button callbacks to set the command
        # The main loop will check this variable
        if hasattr(self, '_current_command_var'):
            self._current_command_var.set(command)
    
    
    def _validate_navigation(self, gui_components):
        """Validate that navigation is safe (no unsaved changes)"""
        # Check if there are any unsaved changes in the form
        first_name = gui_components['first_name_var'].get().strip()
        last_name = gui_components['last_name_var'].get().strip()
        date_of_birth = gui_components['date_of_birth_var'].get().strip()
        
        # If all three required fields are filled, ask for confirmation
        if first_name and last_name and date_of_birth:
            result = messagebox.askyesnocancel(
                "Unsaved Changes",
                "You have unsaved changes in the identification form.\n\n"
                "Do you want to save them before continuing?\n\n"
                "• Yes: Save current identification and continue\n"
                "• No: Discard changes and continue\n"
                "• Cancel: Stay on current face"
            )
            
            if result is True:  # Yes - Save and continue
                return 'save_and_continue'
            elif result is False:  # No - Discard and continue
                return 'discard_and_continue'
            else:  # Cancel - Don't navigate
                return 'cancel'
        
        return 'continue'  # No changes, safe to continue
    
    def _clear_form(self, gui_components):
        """Clear all form fields and similar face selections"""
        gui_components['first_name_var'].set("")
        gui_components['last_name_var'].set("")
        gui_components['middle_name_var'].set("")
        gui_components['maiden_name_var'].set("")
        gui_components['date_of_birth_var'].set("")
        
        # Clear selected similar faces
        if hasattr(self, '_similar_face_vars'):
            for face_id, var in self._similar_face_vars:
                var.set(False)
    
    def _get_form_data(self, gui_components):
        """Get current form data as a dictionary"""
        return {
            'first_name': gui_components['first_name_var'].get().strip(),
            'last_name': gui_components['last_name_var'].get().strip(),
            'middle_name': gui_components['middle_name_var'].get().strip(),
            'maiden_name': gui_components['maiden_name_var'].get().strip(),
            'date_of_birth': gui_components['date_of_birth_var'].get().strip()
        }
    
    def _get_selected_similar_faces(self):
        """Get list of selected similar face IDs"""
        selected_faces = []
        if hasattr(self, '_similar_face_vars'):
            for face_id, var in self._similar_face_vars:
                if var.get():  # If checkbox is checked
                    selected_faces.append(face_id)
        return selected_faces
    
    def _set_form_data(self, gui_components, form_data):
        """Set form data from a dictionary"""
        gui_components['first_name_var'].set(form_data.get('first_name', ''))
        gui_components['last_name_var'].set(form_data.get('last_name', ''))
        gui_components['middle_name_var'].set(form_data.get('middle_name', ''))
        gui_components['maiden_name_var'].set(form_data.get('maiden_name', ''))
        gui_components['date_of_birth_var'].set(form_data.get('date_of_birth', ''))
    
    def _validate_form_data(self, form_data):
        """Validate that form data is complete enough for identification"""
        first_name = form_data.get('first_name', '').strip()
        last_name = form_data.get('last_name', '').strip()
        date_of_birth = form_data.get('date_of_birth', '').strip()
        
        # Need at least first name or last name, and date of birth
        if not (first_name or last_name):
            return False, "Please enter at least a first name or last name"
        
        if not date_of_birth:
            return False, "Please enter a date of birth"
        
        # Validate date format
        try:
            from datetime import datetime
            datetime.strptime(date_of_birth, '%Y-%m-%d')
        except ValueError:
            return False, "Please enter date of birth in YYYY-MM-DD format"
        
        return True, "Valid"
    
    def _filter_unique_faces_from_list(self, faces_list: List[tuple]) -> List[tuple]:
        """Filter face list to show only unique ones, hiding duplicates with high/medium confidence matches"""
        if not faces_list:
            return faces_list
        
        # Extract face IDs from the list
        face_ids = [face_tuple[0] for face_tuple in faces_list]
        
        # Get face encodings from database for all faces
        face_encodings = {}
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            placeholders = ','.join('?' * len(face_ids))
            cursor.execute(f'''
                SELECT id, encoding 
                FROM faces 
                WHERE id IN ({placeholders}) AND encoding IS NOT NULL
            ''', face_ids)
            
            for face_id, encoding_blob in cursor.fetchall():
                try:
                    import numpy as np
                    # Load encoding as numpy array (not pickle)
                    encoding = np.frombuffer(encoding_blob, dtype=np.float64)
                    face_encodings[face_id] = encoding
                except Exception:
                    continue
        
        # If we don't have enough encodings, return original list
        if len(face_encodings) < 2:
            return faces_list
        
        # Calculate distances between all faces using existing encodings
        face_distances = {}
        face_id_list = list(face_encodings.keys())
        
        for i, face_id1 in enumerate(face_id_list):
            for j, face_id2 in enumerate(face_id_list):
                if i != j:
                    try:
                        import face_recognition
                        encoding1 = face_encodings[face_id1]
                        encoding2 = face_encodings[face_id2]
                        
                        # Calculate distance
                        distance = face_recognition.face_distance([encoding1], encoding2)[0]
                        face_distances[(face_id1, face_id2)] = distance
                    except Exception:
                        # If calculation fails, assume no match
                        face_distances[(face_id1, face_id2)] = 1.0
        
        # Apply unique faces filtering
        unique_faces = []
        seen_face_groups = set()
        
        for face_tuple in faces_list:
            face_id = face_tuple[0]
            
            # Skip if we don't have encoding for this face
            if face_id not in face_encodings:
                unique_faces.append(face_tuple)
                continue
            
            # Find all faces that match this one with high/medium confidence
            matching_face_ids = set([face_id])  # Include self
            for other_face_id in face_encodings.keys():
                if other_face_id != face_id:
                    distance = face_distances.get((face_id, other_face_id), 1.0)
                    confidence_pct = (1 - distance) * 100
                    
                    # If this face matches with high/medium confidence
                    if 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_tuple)
        
        return unique_faces
    
    def _on_unique_faces_change(self, gui_components, original_faces, i, face_status, 
                               date_from, date_to, date_processed_from, date_processed_to):
        """Handle unique faces checkbox change"""
        if gui_components['unique_var'].get():
            # Show progress message
            print("🔄 Applying unique faces filter...")
            
            # Apply unique faces filtering to the main face list
            try:
                filtered_faces = self._filter_unique_faces_from_list(original_faces)
                print(f"✅ Filter applied: {len(filtered_faces)} unique faces remaining")
                
                # Update the original_faces list with filtered results
                # We need to return the filtered list to update the caller
                return filtered_faces
                
            except Exception as e:
                print(f"⚠️  Error applying filter: {e}")
                # Revert checkbox state
                gui_components['unique_var'].set(False)
                return original_faces
        else:
            # Reload the original unfiltered face list
            print("🔄 Reloading all faces...")
            
            # Get fresh unfiltered faces from database
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                query = '''
                    SELECT f.id, f.photo_id, p.path, p.filename, f.location 
                    FROM faces f 
                    JOIN photos p ON f.photo_id = p.id 
                    WHERE f.person_id IS NULL 
                '''
                params = []
                
                # Add date taken filtering if specified
                if date_from:
                    query += ' AND p.date_taken >= ?'
                    params.append(date_from)
                
                if date_to:
                    query += ' AND p.date_taken <= ?'
                    params.append(date_to)
                
                # Add date processed filtering if specified
                if date_processed_from:
                    query += ' AND DATE(p.date_added) >= ?'
                    params.append(date_processed_from)
                
                if date_processed_to:
                    query += ' AND DATE(p.date_added) <= ?'
                    params.append(date_processed_to)
                
                cursor.execute(query, params)
                unfiltered_faces = cursor.fetchall()
            
            print(f"✅ Reloaded: {len(unfiltered_faces)} total faces")
            return unfiltered_faces
    
    def _on_compare_change(self, gui_components, face_id, tolerance, face_status, 
                          face_selection_states, identify_data_cache, original_faces, i):
        """Handle compare checkbox change"""
        # Toggle panel visibility
        self._toggle_similar_faces_panel(gui_components)
        
        # Update similar faces if compare is now enabled
        if gui_components['compare_var'].get():
            self._update_similar_faces(gui_components, face_id, tolerance, face_status, 
                                     face_selection_states, identify_data_cache, original_faces, i)