#!/usr/bin/env python3
"""
Auto-match face identification GUI implementation for PunimTag
"""

import os
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_FACE_TOLERANCE
from database import DatabaseManager
from face_processing import FaceProcessor
from gui_core import GUICore


class AutoMatchGUI:
    """Handles the auto-match face identification GUI interface"""
    
    def __init__(self, db_manager: DatabaseManager, face_processor: FaceProcessor, verbose: int = 0):
        """Initialize the auto-match GUI"""
        self.db = db_manager
        self.face_processor = face_processor
        self.verbose = verbose
        self.gui_core = GUICore()
    
    def auto_identify_matches(self, tolerance: float = DEFAULT_FACE_TOLERANCE, confirm: bool = True, 
                             show_faces: bool = False, include_same_photo: bool = False) -> int:
        """Automatically identify faces that match already identified faces using GUI"""
        # Get all identified faces (one per person) to use as reference faces
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT f.id, f.person_id, f.photo_id, f.location, p.filename, f.quality_score
                FROM faces f
                JOIN photos p ON f.photo_id = p.id
                WHERE f.person_id IS NOT NULL AND f.quality_score >= 0.3
                ORDER BY f.person_id, f.quality_score DESC
            ''')
            identified_faces = cursor.fetchall()
        
        if not identified_faces:
            print("🔍 No identified faces found for auto-matching")
            return 0
        
        # Group by person and get the best quality face per person
        person_faces = {}
        for face in identified_faces:
            person_id = face[1]
            if person_id not in person_faces:
                person_faces[person_id] = face
        
        # Convert to ordered list to ensure consistent ordering
        # Order by person name for user-friendly consistent results across runs
        person_faces_list = []
        for person_id, face in person_faces.items():
            # Get person name for ordering
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('SELECT first_name, last_name FROM people WHERE id = ?', (person_id,))
                result = cursor.fetchone()
                if result:
                    first_name, last_name = result
                    if last_name and first_name:
                        person_name = f"{last_name}, {first_name}"
                    elif last_name:
                        person_name = last_name
                    elif first_name:
                        person_name = first_name
                    else:
                        person_name = "Unknown"
                else:
                    person_name = "Unknown"
            person_faces_list.append((person_id, face, person_name))
        
        # Sort by person name for consistent, user-friendly ordering
        person_faces_list.sort(key=lambda x: x[2])  # Sort by person name (index 2)
        
        print(f"\n🎯 Found {len(person_faces)} identified people to match against")
        print("📊 Confidence Guide: 🟢80%+ = Very High, 🟡70%+ = High, 🟠60%+ = Medium, 🔴50%+ = Low, ⚫<50% = Very Low")
        
        # Find similar faces for each identified person using face-to-face comparison
        matches_by_matched = {}
        for person_id, reference_face, person_name in person_faces_list:
            reference_face_id = reference_face[0]
            
            # Use the same filtering and sorting logic as identify
            similar_faces = self.face_processor._get_filtered_similar_faces(reference_face_id, tolerance, include_same_photo, face_status=None)
            
            # Convert to auto-match format
            person_matches = []
            for similar_face in similar_faces:
                # Convert to auto-match format
                match = {
                    'unidentified_id': similar_face['face_id'],
                    'unidentified_photo_id': similar_face['photo_id'],
                    'unidentified_filename': similar_face['filename'],
                    'unidentified_location': similar_face['location'],
                    'matched_id': reference_face_id,
                    'matched_photo_id': reference_face[2],
                    'matched_filename': reference_face[4],
                    'matched_location': reference_face[3],
                    'person_id': person_id,
                    'distance': similar_face['distance'],
                    'quality_score': similar_face['quality_score'],
                    'adaptive_tolerance': similar_face.get('adaptive_tolerance', tolerance)
                }
                person_matches.append(match)
            
            matches_by_matched[person_id] = person_matches
        
        # Flatten all matches for counting
        all_matches = []
        for person_matches in matches_by_matched.values():
            all_matches.extend(person_matches)
        
        if not all_matches:
            print("🔍 No similar faces found for auto-identification")
            return 0
        
        print(f"\n🎯 Found {len(all_matches)} potential matches")
        
        # Pre-fetch all needed data to avoid repeated database queries in update_display
        print("📊 Pre-fetching data for optimal performance...")
        data_cache = self._prefetch_auto_match_data(matches_by_matched)
        
        print(f"✅ Pre-fetched {len(data_cache.get('person_details', {}))} person details and {len(data_cache.get('photo_paths', {}))} photo paths")
        
        identified_count = 0
        
        # Create the main window
        root = tk.Tk()
        root.title("Auto-Match Face Identification")
        root.resizable(True, True)
        
        # Track window state to prevent multiple destroy calls
        window_destroyed = False
        
        # Hide window initially to prevent flash at corner
        root.withdraw()
        
        # Set up protocol handler for window close button (X)
        def on_closing():
            nonlocal window_destroyed
            # Clean up face crops and caches
            self.face_processor.cleanup_face_crops()
            self.db.close_db_connection()
            
            if not window_destroyed:
                window_destroyed = True
                try:
                    root.destroy()
                except tk.TclError:
                    pass  # Window already destroyed
        
        root.protocol("WM_DELETE_WINDOW", on_closing)
        
        # Set up window size saving with larger default size
        saved_size = self.gui_core.setup_window_size_saving(root, "gui_config.json")
        # Override with larger size for auto-match window
        root.geometry("1000x700")
        
        # 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)
        main_frame.columnconfigure(1, weight=1)
        
        # Left side - identified person
        left_frame = ttk.LabelFrame(main_frame, text="Identified person", padding="10")
        left_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
        
        # Right side - unidentified faces that match this person
        right_frame = ttk.LabelFrame(main_frame, text="Unidentified Faces to Match", padding="10")
        right_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
        
        # Configure row weights
        main_frame.rowconfigure(0, weight=1)
        
        # Check if there's only one person - if so, disable search functionality
        # Use matched_ids instead of person_faces_list since we only show people with potential matches
        matched_ids = [person_id for person_id, _, _ in person_faces_list if person_id in matches_by_matched and matches_by_matched[person_id]]
        has_only_one_person = len(matched_ids) == 1
        print(f"DEBUG: person_faces_list length: {len(person_faces_list)}, matched_ids length: {len(matched_ids)}, has_only_one_person: {has_only_one_person}")
        
        # Search controls for filtering people by last name
        last_name_search_var = tk.StringVar()
        # Search field with label underneath (like modifyidentified edit section)
        search_frame = ttk.Frame(left_frame)
        search_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # Search input on the left
        search_entry = ttk.Entry(search_frame, textvariable=last_name_search_var, width=20)
        search_entry.grid(row=0, column=0, sticky=tk.W)
        
        # Buttons on the right of the search input
        buttons_row = ttk.Frame(search_frame)
        buttons_row.grid(row=0, column=1, sticky=tk.W, padx=(6, 0))
        
        search_btn = ttk.Button(buttons_row, text="Search", width=8)
        search_btn.pack(side=tk.LEFT, padx=(0, 5))
        clear_btn = ttk.Button(buttons_row, text="Clear", width=6)
        clear_btn.pack(side=tk.LEFT)
        
        # Helper label directly under the search input
        if has_only_one_person:
            print("DEBUG: Disabling search functionality - only one person found")
            # Disable search functionality if there's only one person
            search_entry.config(state='disabled')
            search_btn.config(state='disabled')
            clear_btn.config(state='disabled')
            # Add a label to explain why search is disabled
            disabled_label = ttk.Label(search_frame, text="(Search disabled - only one person found)", 
                                     font=("Arial", 8), foreground="gray")
            disabled_label.grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=(2, 0))
        else:
            print("DEBUG: Search functionality enabled - multiple people found")
            # Normal helper label when search is enabled
            last_name_label = ttk.Label(search_frame, text="Type Last Name", font=("Arial", 8), foreground="gray")
            last_name_label.grid(row=1, column=0, sticky=tk.W, pady=(2, 0))
        
        # Matched person info
        matched_info_label = ttk.Label(left_frame, text="", font=("Arial", 10, "bold"))
        matched_info_label.grid(row=2, column=0, pady=(0, 10), sticky=tk.W)
        
        # Matched person image
        style = ttk.Style()
        canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
        matched_canvas = tk.Canvas(left_frame, width=300, height=300, bg=canvas_bg_color, highlightthickness=0)
        matched_canvas.grid(row=3, column=0, pady=(0, 10))
        
        # Save button for this person (will be created after function definitions)
        save_btn = None
        
        # Matches scrollable frame
        matches_frame = ttk.Frame(right_frame)
        matches_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Control buttons for matches (Select All / Clear All)
        matches_controls_frame = ttk.Frame(matches_frame)
        matches_controls_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=5, pady=(5, 0))
        
        def select_all_matches():
            """Select all match checkboxes"""
            for var in match_vars:
                var.set(True)
        
        def clear_all_matches():
            """Clear all match checkboxes"""
            for var in match_vars:
                var.set(False)
        
        select_all_matches_btn = ttk.Button(matches_controls_frame, text="☑️ Select All", command=select_all_matches)
        select_all_matches_btn.pack(side=tk.LEFT, padx=(0, 5))
        
        clear_all_matches_btn = ttk.Button(matches_controls_frame, text="☐ Clear All", command=clear_all_matches)
        clear_all_matches_btn.pack(side=tk.LEFT)
        
        def update_match_control_buttons_state():
            """Enable/disable Select All / Clear All based on matches presence"""
            if match_vars:
                select_all_matches_btn.config(state='normal')
                clear_all_matches_btn.config(state='normal')
            else:
                select_all_matches_btn.config(state='disabled')
                clear_all_matches_btn.config(state='disabled')
        
        # Create scrollbar for matches
        scrollbar = ttk.Scrollbar(right_frame, orient="vertical", command=None)
        scrollbar.grid(row=0, column=1, rowspan=1, sticky=(tk.N, tk.S))
        
        # Create canvas for matches with scrollbar
        style = ttk.Style()
        canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
        matches_canvas = tk.Canvas(matches_frame, yscrollcommand=scrollbar.set, bg=canvas_bg_color, highlightthickness=0)
        matches_canvas.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        scrollbar.config(command=matches_canvas.yview)
        
        # Configure grid weights
        right_frame.columnconfigure(0, weight=1)
        right_frame.rowconfigure(0, weight=1)
        matches_frame.columnconfigure(0, weight=1)
        matches_frame.rowconfigure(0, weight=0)  # Controls row takes minimal space
        matches_frame.rowconfigure(1, weight=1)  # Canvas row expandable
        
        # Control buttons (navigation only)
        control_frame = ttk.Frame(main_frame)
        control_frame.grid(row=1, column=0, columnspan=2, pady=(10, 0))
        
        # Button commands
        current_matched_index = 0
        matched_ids = [person_id for person_id, _, _ in person_faces_list if person_id in matches_by_matched and matches_by_matched[person_id]]
        filtered_matched_ids = None  # filtered subset based on last name search
        
        match_checkboxes = []
        match_vars = []
        identified_faces_per_person = {}  # Track which faces were identified for each person
        checkbox_states_per_person = {}  # Track checkbox states for each person (unsaved selections)
        original_checkbox_states_per_person = {}  # Track original/default checkbox states for comparison
        
        def on_confirm_matches():
            nonlocal identified_count, current_matched_index, identified_faces_per_person
            active_ids = filtered_matched_ids if filtered_matched_ids is not None else matched_ids
            if current_matched_index < len(active_ids):
                matched_id = active_ids[current_matched_index]
                matches_for_this_person = matches_by_matched[matched_id]
                
                # Initialize identified faces for this person if not exists
                if matched_id not in identified_faces_per_person:
                    identified_faces_per_person[matched_id] = set()
                
                with self.db.get_db_connection() as conn:
                    cursor = conn.cursor()
                    
                    # Process all matches (both checked and unchecked)
                    for i, (match, var) in enumerate(zip(matches_for_this_person, match_vars)):
                        if var.get():
                            # Face is checked - assign to person
                            cursor.execute(
                                'UPDATE faces SET person_id = ? WHERE id = ?',
                                (match['person_id'], match['unidentified_id'])
                            )
                            
                            # Use cached person name instead of database query
                            person_details = data_cache['person_details'].get(match['person_id'], {})
                            person_name = person_details.get('full_name', "Unknown")
                            
                            # Track this face as identified for this person
                            identified_faces_per_person[matched_id].add(match['unidentified_id'])
                            
                            print(f"✅ Identified as: {person_name}")
                            identified_count += 1
                        else:
                            # Face is unchecked - check if it was previously identified for this person
                            if match['unidentified_id'] in identified_faces_per_person[matched_id]:
                                # This face was previously identified for this person, now unchecking it
                                cursor.execute(
                                    'UPDATE faces SET person_id = NULL WHERE id = ?',
                                    (match['unidentified_id'],)
                                )
                                
                                # Remove from identified faces for this person
                                identified_faces_per_person[matched_id].discard(match['unidentified_id'])
                                
                                print(f"❌ Unidentified: {match['unidentified_filename']}")
                
                # Update person encodings for all affected persons after database transaction is complete
                for person_id in set(match['person_id'] for match in matches_for_this_person if match['person_id']):
                    self.face_processor.update_person_encodings(person_id)
                
                # After saving, set original states to the current UI states so there are no unsaved changes
                current_snapshot = {}
                for match, var in zip(matches_for_this_person, match_vars):
                    unique_key = f"{matched_id}_{match['unidentified_id']}"
                    current_snapshot[unique_key] = var.get()
                checkbox_states_per_person[matched_id] = dict(current_snapshot)
                original_checkbox_states_per_person[matched_id] = dict(current_snapshot)
        
        def on_skip_current():
            nonlocal current_matched_index
            # Save current checkbox states before navigating away
            save_current_checkbox_states()
            current_matched_index += 1
            active_ids = filtered_matched_ids if filtered_matched_ids is not None else matched_ids
            if current_matched_index < len(active_ids):
                update_display()
            else:
                finish_auto_match()
        
        def on_go_back():
            nonlocal current_matched_index
            if current_matched_index > 0:
                # Save current checkbox states before navigating away
                save_current_checkbox_states()
                current_matched_index -= 1
                update_display()
        
        def has_unsaved_changes():
            """Check if there are any unsaved changes by comparing current states with original states"""
            for person_id, current_states in checkbox_states_per_person.items():
                if person_id in original_checkbox_states_per_person:
                    original_states = original_checkbox_states_per_person[person_id]
                    # Check if any checkbox state differs from its original state
                    for key, current_value in current_states.items():
                        if key not in original_states or original_states[key] != current_value:
                            return True
                else:
                    # If person has current states but no original states, there are changes
                    if any(current_states.values()):
                        return True
            return False
        
        def apply_last_name_filter():
            """Filter people by last name and update navigation"""
            nonlocal filtered_matched_ids, current_matched_index
            query = last_name_search_var.get().strip().lower()
            if query:
                # Filter person_faces_list by last name
                filtered_people = []
                for person_id, face, person_name in person_faces_list:
                    # Extract last name from person_name (format: "Last, First")
                    if ',' in person_name:
                        last_name = person_name.split(',')[0].strip().lower()
                    else:
                        last_name = person_name.strip().lower()
                    
                    if query in last_name:
                        filtered_people.append((person_id, face, person_name))
                
                # Get filtered matched_ids
                filtered_matched_ids = [person_id for person_id, _, _ in filtered_people if person_id in matches_by_matched and matches_by_matched[person_id]]
            else:
                filtered_matched_ids = None
            
            # Reset to first person in filtered list
            current_matched_index = 0
            if filtered_matched_ids:
                update_display()
            else:
                # No matches - clear display
                matched_info_label.config(text="No people match filter")
                matched_canvas.delete("all")
                matched_canvas.create_text(150, 150, text="No matches found", fill="gray")
                matches_canvas.delete("all")
                update_button_states()
        
        def clear_last_name_filter():
            """Clear filter and show all people"""
            nonlocal filtered_matched_ids, current_matched_index
            last_name_search_var.set("")
            filtered_matched_ids = None
            current_matched_index = 0
            update_display()
        
        def on_quit_auto_match():
            nonlocal window_destroyed
            # Check for unsaved changes before quitting
            if has_unsaved_changes():
                result = messagebox.askyesnocancel(
                    "Unsaved Changes",
                    "You have unsaved changes that will be lost if you quit.\n\n"
                    "Yes: Save current changes and quit\n"
                    "No: Quit without saving\n"
                    "Cancel: Return to auto-match"
                )
                if result is None:
                    # Cancel
                    return
                if result:
                    # Save current person's changes, then quit
                    save_current_checkbox_states()
                    on_confirm_matches()
            if not window_destroyed:
                window_destroyed = True
                try:
                    root.destroy()
                except tk.TclError:
                    pass  # Window already destroyed
        
        def finish_auto_match():
            nonlocal window_destroyed
            print(f"\n✅ Auto-identified {identified_count} faces")
            if not window_destroyed:
                window_destroyed = True
                try:
                    root.destroy()
                except tk.TclError:
                    pass  # Window already destroyed
        
        # Create button references for state management
        back_btn = ttk.Button(control_frame, text="⏮️ Back", command=on_go_back)
        next_btn = ttk.Button(control_frame, text="⏭️ Next", command=on_skip_current)
        quit_btn = ttk.Button(control_frame, text="❌ Quit", command=on_quit_auto_match)
        
        back_btn.grid(row=0, column=0, padx=(0, 5))
        next_btn.grid(row=0, column=1, padx=5)
        quit_btn.grid(row=0, column=2, padx=(5, 0))
        
        # Create save button now that functions are defined
        save_btn = ttk.Button(left_frame, text="💾 Save Changes", command=on_confirm_matches)
        save_btn.grid(row=4, column=0, pady=(0, 10), sticky=(tk.W, tk.E))
        
        def update_button_states():
            """Update button states based on current position"""
            active_ids = filtered_matched_ids if filtered_matched_ids is not None else matched_ids
            # Enable/disable Back button based on position
            if current_matched_index > 0:
                back_btn.config(state='normal')
            else:
                back_btn.config(state='disabled')
            
            # Enable/disable Next button based on position
            if current_matched_index < len(active_ids) - 1:
                next_btn.config(state='normal')
            else:
                next_btn.config(state='disabled')
        
        def update_save_button_text():
            """Update save button text with current person name"""
            active_ids = filtered_matched_ids if filtered_matched_ids is not None else matched_ids
            if current_matched_index < len(active_ids):
                matched_id = active_ids[current_matched_index]
                # Get person name from the first match for this person
                matches_for_current_person = matches_by_matched[matched_id]
                if matches_for_current_person:
                    person_id = matches_for_current_person[0]['person_id']
                    # Use cached person name instead of database query
                    person_details = data_cache['person_details'].get(person_id, {})
                    person_name = person_details.get('full_name', "Unknown")
                    save_btn.config(text=f"💾 Save changes for {person_name}")
                else:
                    save_btn.config(text="💾 Save Changes")
            else:
                save_btn.config(text="💾 Save Changes")
        
        def save_current_checkbox_states():
            """Save current checkbox states for the current person.
            Note: Do NOT modify original states here to avoid false positives
            when a user toggles and reverts a checkbox.
            """
            active_ids = filtered_matched_ids if filtered_matched_ids is not None else matched_ids
            if current_matched_index < len(active_ids) and match_vars:
                current_matched_id = active_ids[current_matched_index]
                matches_for_current_person = matches_by_matched[current_matched_id]
                
                if len(match_vars) == len(matches_for_current_person):
                    if current_matched_id not in checkbox_states_per_person:
                        checkbox_states_per_person[current_matched_id] = {}
                    
                    # Save current checkbox states for this person
                    for i, (match, var) in enumerate(zip(matches_for_current_person, match_vars)):
                        unique_key = f"{current_matched_id}_{match['unidentified_id']}"
                        current_value = var.get()
                        checkbox_states_per_person[current_matched_id][unique_key] = current_value
                        
                        if self.verbose >= 2:
                            print(f"DEBUG: Saved state for person {current_matched_id}, face {match['unidentified_id']}: {current_value}")
        
        def update_display():
            nonlocal current_matched_index
            active_ids = filtered_matched_ids if filtered_matched_ids is not None else matched_ids
            if current_matched_index >= len(active_ids):
                finish_auto_match()
                return
            
            matched_id = active_ids[current_matched_index]
            matches_for_this_person = matches_by_matched[matched_id]
            
            # Update button states
            update_button_states()
            
            # Update save button text with person name
            update_save_button_text()
            
            # Update title
            active_ids = filtered_matched_ids if filtered_matched_ids is not None else matched_ids
            root.title(f"Auto-Match Face Identification - {current_matched_index + 1}/{len(active_ids)}")
            
            # Get the first match to get matched person info
            if not matches_for_this_person:
                print(f"❌ Error: No matches found for current person {matched_id}")
                # No items on the right panel – disable Select All / Clear All
                match_checkboxes.clear()
                match_vars.clear()
                update_match_control_buttons_state()
                # Skip to next person if available
                if current_matched_index < len(matched_ids) - 1:
                    current_matched_index += 1
                    update_display()
                else:
                    finish_auto_match()
                return
            
            first_match = matches_for_this_person[0]
            
            # Use cached data instead of database queries
            person_details = data_cache['person_details'].get(first_match['person_id'], {})
            person_name = person_details.get('full_name', "Unknown")
            date_of_birth = person_details.get('date_of_birth', '')
            matched_photo_path = data_cache['photo_paths'].get(first_match['matched_photo_id'], None)
            
            # Create detailed person info display
            person_info_lines = [f"👤 Person: {person_name}"]
            if date_of_birth:
                person_info_lines.append(f"📅 Born: {date_of_birth}")
            person_info_lines.extend([
                f"📁 Photo: {first_match['matched_filename']}",
                f"📍 Face location: {first_match['matched_location']}"
            ])
            
            # Update matched person info
            matched_info_label.config(text="\n".join(person_info_lines))
            
            # Display matched person face
            matched_canvas.delete("all")
            if show_faces:
                matched_crop_path = self.face_processor._extract_face_crop(
                    matched_photo_path, 
                    first_match['matched_location'], 
                    f"matched_{first_match['person_id']}"
                )
                
                if matched_crop_path and os.path.exists(matched_crop_path):
                    try:
                        pil_image = Image.open(matched_crop_path)
                        pil_image.thumbnail((300, 300), Image.Resampling.LANCZOS)
                        photo = ImageTk.PhotoImage(pil_image)
                        matched_canvas.create_image(150, 150, image=photo)
                        matched_canvas.image = photo
                        
                        # Add photo icon to the matched person face - exactly in corner
                        # Compute top-left of the centered image for accurate placement
                        actual_width, actual_height = pil_image.size
                        top_left_x = 150 - (actual_width // 2)
                        top_left_y = 150 - (actual_height // 2)
                        self.gui_core.create_photo_icon(matched_canvas, matched_photo_path, icon_size=20,
                                              face_x=top_left_x, face_y=top_left_y, 
                                              face_width=actual_width, face_height=actual_height,
                                              canvas_width=300, canvas_height=300)
                    except Exception as e:
                        matched_canvas.create_text(150, 150, text=f"❌ Could not load image: {e}", fill="red")
                else:
                    matched_canvas.create_text(150, 150, text="🖼️ No face crop available", fill="gray")
            
            # Clear and populate unidentified faces
            matches_canvas.delete("all")
            match_checkboxes.clear()
            match_vars.clear()
            update_match_control_buttons_state()
            
            # Create frame for unidentified faces inside canvas
            matches_inner_frame = ttk.Frame(matches_canvas)
            matches_canvas.create_window((0, 0), window=matches_inner_frame, anchor="nw")
            
            # Use cached photo paths instead of database queries
            photo_paths = data_cache['photo_paths']
            
            # Create all checkboxes
            for i, match in enumerate(matches_for_this_person):
                # Get unidentified face info from cached data
                unidentified_photo_path = photo_paths.get(match['unidentified_photo_id'], '')
                
                # Calculate confidence
                confidence_pct = (1 - match['distance']) * 100
                confidence_desc = self.face_processor._get_confidence_description(confidence_pct)
                
                # Create match frame
                match_frame = ttk.Frame(matches_inner_frame)
                match_frame.grid(row=i, column=0, sticky=(tk.W, tk.E), pady=5)
                
                # Checkbox for this match
                match_var = tk.BooleanVar()
                
                # Restore previous checkbox state if available
                unique_key = f"{matched_id}_{match['unidentified_id']}"
                if matched_id in checkbox_states_per_person and unique_key in checkbox_states_per_person[matched_id]:
                    saved_state = checkbox_states_per_person[matched_id][unique_key]
                    match_var.set(saved_state)
                    if self.verbose >= 2:
                        print(f"DEBUG: Restored state for person {matched_id}, face {match['unidentified_id']}: {saved_state}")
                # Otherwise, pre-select if this face was previously identified for this person
                elif matched_id in identified_faces_per_person and match['unidentified_id'] in identified_faces_per_person[matched_id]:
                    match_var.set(True)
                    if self.verbose >= 2:
                        print(f"DEBUG: Pre-selected identified face for person {matched_id}, face {match['unidentified_id']}")
                
                match_vars.append(match_var)
                
                # Capture original state at render time (once per person per face)
                if matched_id not in original_checkbox_states_per_person:
                    original_checkbox_states_per_person[matched_id] = {}
                if unique_key not in original_checkbox_states_per_person[matched_id]:
                    original_checkbox_states_per_person[matched_id][unique_key] = match_var.get()

                # Add callback to save state immediately when checkbox changes
                def on_checkbox_change(var, person_id, face_id):
                    unique_key = f"{person_id}_{face_id}"
                    if person_id not in checkbox_states_per_person:
                        checkbox_states_per_person[person_id] = {}
                    
                    current_value = var.get()
                    checkbox_states_per_person[person_id][unique_key] = current_value
                    
                    if self.verbose >= 2:
                        print(f"DEBUG: Checkbox changed for person {person_id}, face {face_id}: {current_value}")
                
                # Bind the callback to the variable (avoid late-binding by capturing ids)
                current_person_id = matched_id
                current_face_id = match['unidentified_id']
                match_var.trace('w', lambda *args, var=match_var, person_id=current_person_id, face_id=current_face_id: on_checkbox_change(var, person_id, 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))
                match_checkboxes.append(checkbox)
                
                # Unidentified face image now immediately after checkbox (right panel face to the right of checkbox)
                match_canvas = None
                if show_faces:
                    style = ttk.Style()
                    canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
                    match_canvas = tk.Canvas(match_frame, width=100, height=100, bg=canvas_bg_color, highlightthickness=0)
                    match_canvas.grid(row=0, column=1, rowspan=2, sticky=(tk.W, tk.N), padx=(5, 10))
                    
                    unidentified_crop_path = self.face_processor._extract_face_crop(
                        unidentified_photo_path,
                        match['unidentified_location'],
                        f"unid_{match['unidentified_id']}"
                    )
                    
                    if unidentified_crop_path and os.path.exists(unidentified_crop_path):
                        try:
                            pil_image = Image.open(unidentified_crop_path)
                            pil_image.thumbnail((100, 100), Image.Resampling.LANCZOS)
                            photo = ImageTk.PhotoImage(pil_image)
                            match_canvas.create_image(50, 50, image=photo)
                            match_canvas.image = photo
                            
                            # Add photo icon exactly at top-right of the face area
                            self.gui_core.create_photo_icon(match_canvas, unidentified_photo_path, icon_size=15,
                                                  face_x=0, face_y=0, 
                                                  face_width=100, face_height=100,
                                                  canvas_width=100, canvas_height=100)
                        except Exception:
                            match_canvas.create_text(50, 50, text="❌", fill="red")
                    else:
                        match_canvas.create_text(50, 50, text="🖼️", fill="gray")

                # Confidence badge and filename 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))
                
                badge = self.gui_core.create_confidence_badge(info_container, confidence_pct)
                badge.pack(anchor=tk.W)
                
                filename_label = ttk.Label(info_container, text=f"📁 {match['unidentified_filename']}", font=("Arial", 8), foreground="gray")
                filename_label.pack(anchor=tk.W, pady=(2, 0))
            
            # Update Select All / Clear All button states after populating
            update_match_control_buttons_state()
            
            # Update scroll region
            matches_canvas.update_idletasks()
            matches_canvas.configure(scrollregion=matches_canvas.bbox("all"))
        
        # Show the window
        try:
            root.deiconify()
            root.lift()
            root.focus_force()
        except tk.TclError:
            # Window was destroyed before we could show it
            return 0
        
        # Wire up search controls now that helper functions exist
        try:
            search_btn.config(command=lambda: apply_last_name_filter())
            clear_btn.config(command=lambda: clear_last_name_filter())
            search_entry.bind('<Return>', lambda e: apply_last_name_filter())
        except Exception:
            pass
        
        # Start with first matched person
        update_display()
        
        # Main event loop
        try:
            root.mainloop()
        except tk.TclError:
            pass  # Window was destroyed
        
        return identified_count
    
    def _prefetch_auto_match_data(self, matches_by_matched: Dict) -> Dict:
        """Pre-fetch all needed data to avoid repeated database queries"""
        data_cache = {}
        
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            
            # Pre-fetch all person names and details
            person_ids = list(matches_by_matched.keys())
            if person_ids:
                placeholders = ','.join('?' * len(person_ids))
                cursor.execute(f'SELECT id, first_name, last_name, middle_name, maiden_name, date_of_birth FROM people WHERE id IN ({placeholders})', person_ids)
                data_cache['person_details'] = {}
                for row in cursor.fetchall():
                    person_id = row[0]
                    first_name = row[1] or ''
                    last_name = row[2] or ''
                    middle_name = row[3] or ''
                    maiden_name = row[4] or ''
                    date_of_birth = row[5] or ''
                    
                    # Create full name display
                    name_parts = []
                    if first_name:
                        name_parts.append(first_name)
                    if middle_name:
                        name_parts.append(middle_name)
                    if last_name:
                        name_parts.append(last_name)
                    if maiden_name:
                        name_parts.append(f"({maiden_name})")
                    
                    full_name = ' '.join(name_parts)
                    data_cache['person_details'][person_id] = {
                        'full_name': full_name,
                        'first_name': first_name,
                        'last_name': last_name,
                        'middle_name': middle_name,
                        'maiden_name': maiden_name,
                        'date_of_birth': date_of_birth
                    }
            
            # Pre-fetch all photo paths (both matched and unidentified)
            all_photo_ids = set()
            for person_matches in matches_by_matched.values():
                for match in person_matches:
                    all_photo_ids.add(match['matched_photo_id'])
                    all_photo_ids.add(match['unidentified_photo_id'])
            
            if all_photo_ids:
                photo_ids_list = list(all_photo_ids)
                placeholders = ','.join('?' * len(photo_ids_list))
                cursor.execute(f'SELECT id, path FROM photos WHERE id IN ({placeholders})', photo_ids_list)
                data_cache['photo_paths'] = {row[0]: row[1] for row in cursor.fetchall()}
        
        return data_cache

