#!/usr/bin/env python3
"""
PunimTag CLI - Minimal Photo Face Tagger
Simple command-line tool for face recognition and photo tagging
"""

import os
import sqlite3
import argparse
import face_recognition
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from PIL.ExifTags import TAGS
import pickle
import numpy as np
from typing import List, Dict, Tuple, Optional
import sys
import tempfile
import subprocess
import threading
import time
from datetime import datetime
from functools import lru_cache
from contextlib import contextmanager


class PhotoTagger:
    def __init__(self, db_path: str = "data/photos.db", verbose: int = 0, debug: bool = False):
        """Initialize the photo tagger with database"""
        self.db_path = db_path
        self.verbose = verbose
        self.debug = debug
        self._face_encoding_cache = {}
        self._image_cache = {}
        self._db_connection = None
        self._db_lock = threading.Lock()
        self.init_database()
    
    @contextmanager
    def get_db_connection(self):
        """Context manager for database connections with connection pooling"""
        with self._db_lock:
            if self._db_connection is None:
                self._db_connection = sqlite3.connect(self.db_path)
                self._db_connection.row_factory = sqlite3.Row
            try:
                yield self._db_connection
            except Exception:
                self._db_connection.rollback()
                raise
            else:
                self._db_connection.commit()
    
    def close_db_connection(self):
        """Close database connection"""
        with self._db_lock:
            if self._db_connection:
                self._db_connection.close()
                self._db_connection = None
    
    @lru_cache(maxsize=1000)
    def _get_cached_face_encoding(self, face_id: int, encoding_bytes: bytes) -> np.ndarray:
        """Cache face encodings to avoid repeated numpy conversions"""
        return np.frombuffer(encoding_bytes, dtype=np.float64)
    
    def _clear_caches(self):
        """Clear all caches to free memory"""
        self._face_encoding_cache.clear()
        self._image_cache.clear()
        self._get_cached_face_encoding.cache_clear()
    
    def cleanup(self):
        """Clean up resources and close connections"""
        self._clear_caches()
        self.close_db_connection()
    
    def _cleanup_face_crops(self, current_face_crop_path=None):
        """Clean up face crop files and caches"""
        # Clean up current face crop if provided
        if current_face_crop_path and os.path.exists(current_face_crop_path):
            try:
                os.remove(current_face_crop_path)
            except:
                pass  # Ignore cleanup errors
        
        # Clean up all cached face crop files
        for cache_key, cached_path in list(self._image_cache.items()):
            if os.path.exists(cached_path):
                try:
                    os.remove(cached_path)
                except:
                    pass  # Ignore cleanup errors
        
        # Clear caches
        self._clear_caches()
    
    def _deduplicate_tags(self, tag_list):
        """Remove duplicate tags from a list while preserving order (case insensitive)"""
        seen = set()
        unique_tags = []
        for tag in tag_list:
            if tag.lower() not in seen:
                seen.add(tag.lower())
                unique_tags.append(tag)
        return unique_tags
    
    def _parse_tags_string(self, tags_string):
        """Parse a comma-separated tags string into a list, handling empty strings and whitespace"""
        if not tags_string or tags_string.strip() == "":
            return []
        # Split by comma and strip whitespace from each tag
        tags = [tag.strip() for tag in tags_string.split(",")]
        # Remove empty strings that might result from splitting
        return [tag for tag in tags if tag]
    
    def _get_tag_id_by_name(self, tag_name, tag_name_to_id_map):
        """Get tag ID by name, creating the tag if it doesn't exist"""
        if tag_name in tag_name_to_id_map:
            return tag_name_to_id_map[tag_name]
        return None
    
    def _get_tag_name_by_id(self, tag_id, tag_id_to_name_map):
        """Get tag name by ID"""
        return tag_id_to_name_map.get(tag_id, f"Unknown Tag {tag_id}")
    
    def _load_tag_mappings(self):
        """Load tag name to ID and ID to name mappings from database"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('SELECT id, tag_name FROM tags ORDER BY tag_name')
            tag_id_to_name = {}
            tag_name_to_id = {}
            for row in cursor.fetchall():
                tag_id, tag_name = row
                tag_id_to_name[tag_id] = tag_name
                tag_name_to_id[tag_name] = tag_id
            return tag_id_to_name, tag_name_to_id
    
    def _get_existing_tag_ids_for_photo(self, photo_id):
        """Get list of tag IDs for a photo from database"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT ptl.tag_id 
                FROM phototaglinkage ptl 
                WHERE ptl.photo_id = ?
                ORDER BY ptl.created_date
            ''', (photo_id,))
            return [row[0] for row in cursor.fetchall()]
    
    def _setup_window_size_saving(self, root, config_file="gui_config.json"):
        """Set up window size saving functionality"""
        import json
        import tkinter as tk
        
        # Load saved window size
        default_size = "600x500"
        saved_size = default_size
        
        if os.path.exists(config_file):
            try:
                with open(config_file, 'r') as f:
                    config = json.load(f)
                    saved_size = config.get('window_size', default_size)
            except:
                saved_size = default_size
        
        # Calculate center position before showing window
        try:
            width = int(saved_size.split('x')[0])
            height = int(saved_size.split('x')[1])
            x = (root.winfo_screenwidth() // 2) - (width // 2)
            y = (root.winfo_screenheight() // 2) - (height // 2)
            root.geometry(f"{saved_size}+{x}+{y}")
        except tk.TclError:
            # Fallback to default geometry if positioning fails
            root.geometry(saved_size)
        
        # Track previous size to detect actual resizing
        last_size = None
        
        def save_window_size(event=None):
            nonlocal last_size
            if event and event.widget == root:
                current_size = f"{root.winfo_width()}x{root.winfo_height()}"
                # Only save if size actually changed
                if current_size != last_size:
                    last_size = current_size
                    try:
                        config = {'window_size': current_size}
                        with open(config_file, 'w') as f:
                            json.dump(config, f)
                    except:
                        pass  # Ignore save errors
        
        # Bind resize event
        root.bind('<Configure>', save_window_size)
        return saved_size
    
    def init_database(self):
        """Create database tables if they don't exist"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            
            # Photos table
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS photos (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    path TEXT UNIQUE NOT NULL,
                    filename TEXT NOT NULL,
                    date_added DATETIME DEFAULT CURRENT_TIMESTAMP,
                    date_taken DATE,
                    processed BOOLEAN DEFAULT 0
                )
            ''')
            
            # People table
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS people (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    first_name TEXT NOT NULL,
                    last_name TEXT NOT NULL,
                    middle_name TEXT,
                    maiden_name TEXT,
                    date_of_birth DATE,
                    created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
                    UNIQUE(first_name, last_name, middle_name, maiden_name, date_of_birth)
                )
            ''')
            
            # Faces table
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS faces (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    photo_id INTEGER NOT NULL,
                    person_id INTEGER,
                    encoding BLOB NOT NULL,
                    location TEXT NOT NULL,
                    confidence REAL DEFAULT 0.0,
                    quality_score REAL DEFAULT 0.0,
                    is_primary_encoding BOOLEAN DEFAULT 0,
                    FOREIGN KEY (photo_id) REFERENCES photos (id),
                    FOREIGN KEY (person_id) REFERENCES people (id)
                )
            ''')
            
            # Person encodings table for multiple encodings per person
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS person_encodings (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    person_id INTEGER NOT NULL,
                    face_id INTEGER NOT NULL,
                    encoding BLOB NOT NULL,
                    quality_score REAL DEFAULT 0.0,
                    created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
                    FOREIGN KEY (person_id) REFERENCES people (id),
                    FOREIGN KEY (face_id) REFERENCES faces (id)
                )
            ''')
            
            # Tags table - holds only tag information
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS tags (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    tag_name TEXT UNIQUE NOT NULL,
                    created_date DATETIME DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            
            # Photo-Tag linkage table
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS phototaglinkage (
                    linkage_id INTEGER PRIMARY KEY AUTOINCREMENT,
                    photo_id INTEGER NOT NULL,
                    tag_id INTEGER NOT NULL,
                    created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
                    FOREIGN KEY (photo_id) REFERENCES photos (id),
                    FOREIGN KEY (tag_id) REFERENCES tags (id),
                    UNIQUE(photo_id, tag_id)
                )
            ''')
            
            # Add indexes for better performance
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_faces_person_id ON faces(person_id)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_faces_photo_id ON faces(photo_id)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_photos_processed ON photos(processed)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_faces_quality ON faces(quality_score)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_person_encodings_person_id ON person_encodings(person_id)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_person_encodings_quality ON person_encodings(quality_score)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_photos_date_taken ON photos(date_taken)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_photos_date_added ON photos(date_added)')
            
            # Migration: Add date_taken column to existing photos table if it doesn't exist
            try:
                cursor.execute('ALTER TABLE photos ADD COLUMN date_taken DATE')
                if self.verbose >= 1:
                    print("✅ Added date_taken column to photos table")
            except Exception:
                # Column already exists, ignore
                pass
            
            # Migration: Add date_added column to existing photos table if it doesn't exist
            try:
                cursor.execute('ALTER TABLE photos ADD COLUMN date_added DATETIME DEFAULT CURRENT_TIMESTAMP')
                if self.verbose >= 1:
                    print("✅ Added date_added column to photos table")
            except Exception:
                # Column already exists, ignore
                pass
            
            
        if self.verbose >= 1:
            print(f"✅ Database initialized: {self.db_path}")
    
    def _extract_photo_date(self, photo_path: str) -> Optional[str]:
        """Extract date taken from photo EXIF data"""
        try:
            with Image.open(photo_path) as image:
                exifdata = image.getexif()
                
                # Look for date taken in EXIF tags
                date_tags = [
                    306,  # DateTime
                    36867,  # DateTimeOriginal
                    36868,  # DateTimeDigitized
                ]
                
                for tag_id in date_tags:
                    if tag_id in exifdata:
                        date_str = exifdata[tag_id]
                        if date_str:
                            # Parse EXIF date format (YYYY:MM:DD HH:MM:SS)
                            try:
                                date_obj = datetime.strptime(date_str, '%Y:%m:%d %H:%M:%S')
                                return date_obj.strftime('%Y-%m-%d')
                            except ValueError:
                                # Try alternative format
                                try:
                                    date_obj = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
                                    return date_obj.strftime('%Y-%m-%d')
                                except ValueError:
                                    continue
                
                return None
        except Exception as e:
            if self.verbose >= 2:
                print(f"   ⚠️  Could not extract date from {os.path.basename(photo_path)}: {e}")
            return None
    
    def scan_folder(self, folder_path: str, recursive: bool = True) -> int:
        """Scan folder for photos and add to database"""
        # BREAKPOINT: Set breakpoint here for debugging
        
        
        if not os.path.exists(folder_path):
            print(f"❌ Folder not found: {folder_path}")
            return 0
        
        photo_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff'}
        
        found_photos = []
        
        # BREAKPOINT: Set breakpoint here for debugging
        
        if recursive:
            for root, dirs, files in os.walk(folder_path):
                for file in files:
                    file_ext = Path(file).suffix.lower()
                    if file_ext in photo_extensions:
                        photo_path = os.path.join(root, file)
                        found_photos.append((photo_path, file))
        else:
            for file in os.listdir(folder_path):
                file_ext = Path(file).suffix.lower()
                if file_ext in photo_extensions:
                    photo_path = os.path.join(folder_path, file)
                    found_photos.append((photo_path, file))
        
        if not found_photos:
            print(f"📁 No photos found in {folder_path}")
            return 0
        
        # Add to database
        # BREAKPOINT: Set breakpoint here for debugging
        
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            added_count = 0
            
            for photo_path, filename in found_photos:
                try:
                    # Extract date taken from EXIF data
                    date_taken = self._extract_photo_date(photo_path)
                    
                    cursor.execute(
                        'INSERT OR IGNORE INTO photos (path, filename, date_taken) VALUES (?, ?, ?)',
                        (photo_path, filename, date_taken)
                    )
                    if cursor.rowcount > 0:
                        added_count += 1
                        if self.verbose >= 2:
                            date_info = f" (taken: {date_taken})" if date_taken else " (no date)"
                            print(f"   📸 Added: {filename}{date_info}")
                    elif self.verbose >= 3:
                        print(f"   📸 Already exists: {filename}")
                except Exception as e:
                    print(f"⚠️  Error adding {filename}: {e}")
        
        
        print(f"📁 Found {len(found_photos)} photos, added {added_count} new photos")
        return added_count
    
    
    def process_faces(self, limit: int = 50, model: str = "hog") -> int:
        """Process unprocessed photos for faces"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            
            cursor.execute(
                'SELECT id, path, filename FROM photos WHERE processed = 0 LIMIT ?',
                (limit,)
            )
            unprocessed = cursor.fetchall()
            
            if not unprocessed:
                print("✅ No unprocessed photos found")
                return 0
            
            print(f"🔍 Processing {len(unprocessed)} photos for faces...")
            processed_count = 0
            
            for photo_id, photo_path, filename in unprocessed:
                if not os.path.exists(photo_path):
                    print(f"❌ File not found: {filename}")
                    cursor.execute('UPDATE photos SET processed = 1 WHERE id = ?', (photo_id,))
                    continue
                
                try:
                    # Load image and find faces
                    if self.verbose >= 1:
                        print(f"📸 Processing: {filename}")
                    elif self.verbose == 0:
                        print(".", end="", flush=True)
                    
                    if self.verbose >= 2:
                        print(f"   🔍 Loading image: {photo_path}")
                    
                    image = face_recognition.load_image_file(photo_path)
                    face_locations = face_recognition.face_locations(image, model=model)
                    
                    if face_locations:
                        face_encodings = face_recognition.face_encodings(image, face_locations)
                        if self.verbose >= 1:
                            print(f"   👤 Found {len(face_locations)} faces")
                        
                        # Save faces to database with quality scores
                        for i, (encoding, location) in enumerate(zip(face_encodings, face_locations)):
                            # Calculate face quality score
                            quality_score = self._calculate_face_quality_score(image, location)
                            
                            cursor.execute(
                                'INSERT INTO faces (photo_id, encoding, location, quality_score) VALUES (?, ?, ?, ?)',
                                (photo_id, encoding.tobytes(), str(location), quality_score)
                            )
                            if self.verbose >= 3:
                                print(f"      Face {i+1}: {location} (quality: {quality_score:.2f})")
                    else:
                        if self.verbose >= 1:
                            print(f"   👤 No faces found")
                        elif self.verbose >= 2:
                            print(f"   👤 {filename}: No faces found")
                    
                    # Mark as processed
                    cursor.execute('UPDATE photos SET processed = 1 WHERE id = ?', (photo_id,))
                    processed_count += 1
                    
                except Exception as e:
                    print(f"❌ Error processing {filename}: {e}")
                    cursor.execute('UPDATE photos SET processed = 1 WHERE id = ?', (photo_id,))
        
        if self.verbose == 0:
            print()  # New line after dots
        print(f"✅ Processed {processed_count} photos")
        return processed_count
    
    def identify_faces(self, batch_size: int = 20, show_faces: bool = False, tolerance: float = 0.6, 
                      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"""
        with self.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)
            
            unidentified = cursor.fetchall()
            
            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 = {}
        
        with self.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()})
        
        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
        
        # Use integrated GUI with image and input
        import tkinter as tk
        from tkinter import ttk, messagebox
        from PIL import Image, ImageTk
        import json
        import os
        
        # Create the main window once
        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()
        
        def save_all_pending_identifications():
            """Save all pending identifications from face_person_names"""
            nonlocal identified_count
            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.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._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}")
                else:
                    # Handle legacy string format - skip for now as it doesn't have complete data
                    pass
            
            if saved_count > 0:
                identified_count += saved_count
                print(f"💾 Saved {saved_count} pending identifications")
            
            return saved_count
        
        # 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 validate_navigation():
                return  # Cancel close
            
            # Check if there are pending identifications (faces with complete data but not yet saved)
            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
                    else:
                        # Handle legacy string format - not considered complete without date of birth
                        pass
            
            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
                    save_all_pending_identifications()
                    command = 'q'
                    waiting_for_input = False
                elif result is False:  # No - Close without saving
                    command = 'q'
                    waiting_for_input = False
                else:  # Cancel - Don't close
                    return
            
            # Clean up face crops and caches
            self._cleanup_face_crops(current_face_crop_path)
            self.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._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)
        
        # Calendar dialog function for date filter
        def open_date_calendar(date_var, title):
            """Open a visual calendar dialog to select date"""
            from datetime import datetime, date, timedelta
            import calendar
            
            # Create calendar window
            calendar_window = tk.Toplevel(root)
            calendar_window.title(title)
            calendar_window.resizable(False, False)
            calendar_window.transient(root)
            calendar_window.grab_set()
            
            # Calculate center position before showing the window
            window_width = 400
            window_height = 400
            screen_width = calendar_window.winfo_screenwidth()
            screen_height = calendar_window.winfo_screenheight()
            x = (screen_width // 2) - (window_width // 2)
            y = (screen_height // 2) - (window_height // 2)
            
            # Set geometry with center position before showing
            calendar_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
            
            # Calendar variables
            current_date = datetime.now()
            
            # Check if there's already a date selected
            existing_date_str = date_var.get().strip()
            if existing_date_str:
                try:
                    existing_date = datetime.strptime(existing_date_str, '%Y-%m-%d').date()
                    display_year = existing_date.year
                    display_month = existing_date.month
                    selected_date = existing_date
                except ValueError:
                    # If existing date is invalid, use current date
                    display_year = current_date.year
                    display_month = current_date.month
                    selected_date = None
            else:
                # Default to current date
                display_year = current_date.year
                display_month = current_date.month
                selected_date = None
            
            # Month names
            month_names = ["January", "February", "March", "April", "May", "June",
                          "July", "August", "September", "October", "November", "December"]
            
            # Create custom style for calendar buttons
            style = ttk.Style()
            style.configure("Calendar.TButton", padding=(2, 2))
            style.map("Calendar.TButton",
                     background=[("active", "#e1e1e1")],
                     relief=[("pressed", "sunken")])
            
            # Main frame
            main_cal_frame = ttk.Frame(calendar_window, padding="10")
            main_cal_frame.pack(fill=tk.BOTH, expand=True)
            
            # Header frame with navigation
            header_frame = ttk.Frame(main_cal_frame)
            header_frame.pack(fill=tk.X, pady=(0, 10))
            
            # Month/Year display and navigation
            nav_frame = ttk.Frame(header_frame)
            nav_frame.pack()
            
            # Month/Year label (created once, updated later)
            month_year_label = ttk.Label(nav_frame, text="", font=("Arial", 12, "bold"))
            month_year_label.pack(side=tk.LEFT, padx=10)
            
            def update_calendar():
                """Update the calendar display"""
                # Update month/year label
                month_year_label.configure(text=f"{month_names[display_month-1]} {display_year}")
                
                # Clear existing calendar
                for widget in calendar_frame.winfo_children():
                    widget.destroy()
                
                # Get calendar data
                cal = calendar.monthcalendar(display_year, display_month)
                
                # Day headers
                day_headers = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
                for i, day in enumerate(day_headers):
                    header_label = ttk.Label(calendar_frame, text=day, font=("Arial", 10, "bold"))
                    header_label.grid(row=0, column=i, padx=2, pady=2, sticky="nsew")
                
                # Calendar days
                for week_num, week in enumerate(cal):
                    for day_num, day in enumerate(week):
                        if day == 0:
                            # Empty cell
                            empty_label = ttk.Label(calendar_frame, text="")
                            empty_label.grid(row=week_num+1, column=day_num, padx=2, pady=2, sticky="nsew")
                        else:
                            # Day button
                            day_date = date(display_year, display_month, day)
                            is_selected = selected_date == day_date
                            is_today = day_date == current_date.date()
                            
                            # Button text and style
                            button_text = str(day)
                            if is_today:
                                button_text = f"•{day}•"  # Mark today
                            
                            day_btn = ttk.Button(calendar_frame, text=button_text, 
                                               style="Calendar.TButton" if not is_selected else "Calendar.TButton",
                                               command=lambda d=day_date: select_date(d))
                            day_btn.grid(row=week_num+1, column=day_num, padx=2, pady=2, sticky="nsew")
                            
                            # Highlight selected date
                            if is_selected:
                                day_btn.configure(style="Calendar.TButton")
                                # Add visual indication of selection
                                day_btn.configure(text=f"[{day}]")
            
            def select_date(selected_day):
                """Select a date and close calendar"""
                nonlocal selected_date
                selected_date = selected_day
                date_var.set(selected_day.strftime('%Y-%m-%d'))
                calendar_window.destroy()
            
            def prev_month():
                nonlocal display_month, display_year
                display_month -= 1
                if display_month < 1:
                    display_month = 12
                    display_year -= 1
                update_calendar()
            
            def next_month():
                nonlocal display_month, display_year
                display_month += 1
                if display_month > 12:
                    display_month = 1
                    display_year += 1
                update_calendar()
            
            def prev_year():
                nonlocal display_year
                display_year -= 1
                update_calendar()
            
            def next_year():
                nonlocal display_year
                display_year += 1
                update_calendar()
            
            # Navigation buttons
            prev_year_btn = ttk.Button(nav_frame, text="<<", width=3, command=prev_year)
            prev_year_btn.pack(side=tk.LEFT)
            
            prev_month_btn = ttk.Button(nav_frame, text="<", width=3, command=prev_month)
            prev_month_btn.pack(side=tk.LEFT, padx=(5, 0))
            
            next_month_btn = ttk.Button(nav_frame, text=">", width=3, command=next_month)
            next_month_btn.pack(side=tk.LEFT, padx=(5, 0))
            
            next_year_btn = ttk.Button(nav_frame, text=">>", width=3, command=next_year)
            next_year_btn.pack(side=tk.LEFT)
            
            # Calendar grid frame
            calendar_frame = ttk.Frame(main_cal_frame)
            calendar_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
            
            # Configure grid weights
            for i in range(7):
                calendar_frame.columnconfigure(i, weight=1)
            for i in range(7):
                calendar_frame.rowconfigure(i, weight=1)
            
            # Buttons frame
            buttons_frame = ttk.Frame(main_cal_frame)
            buttons_frame.pack(fill=tk.X)
            
            def clear_date():
                """Clear the selected date"""
                date_var.set("")
                calendar_window.destroy()
            
            # Clear button
            clear_btn = ttk.Button(buttons_frame, text="Clear", command=clear_date)
            clear_btn.pack(side=tk.LEFT)
            
            # Cancel button
            cancel_btn = ttk.Button(buttons_frame, text="Cancel", command=calendar_window.destroy)
            cancel_btn.pack(side=tk.RIGHT)
            
            # Initial calendar display
            update_calendar()
        
        # Unique faces only checkbox variable (must be defined before widgets that use it)
        unique_faces_var = tk.BooleanVar()
        
        # Define update_similar_faces function first - reusing auto-match display logic
        def update_similar_faces():
            """Update the similar faces panel when compare is enabled - reuses auto-match display logic"""
            nonlocal similar_faces_data, similar_face_vars, similar_face_images, similar_face_crops, face_selection_states
            
            # Note: Selection states are now saved automatically via callbacks (auto-match style)
            
            # Clear existing similar faces
            for widget in similar_scrollable_frame.winfo_children():
                widget.destroy()
            similar_face_vars.clear()
            similar_face_images.clear()
            
            # Clean up existing face crops
            for crop_path in similar_face_crops:
                try:
                    if os.path.exists(crop_path):
                        os.remove(crop_path)
                except:
                    pass
            similar_face_crops.clear()
            
            if compare_var.get():
                # Use the same filtering and sorting logic as auto-match (no unique faces filter for similar faces)
                unidentified_similar_faces = self._get_filtered_similar_faces(face_id, tolerance, include_same_photo=False, face_status=face_status)
                
                if unidentified_similar_faces:
                    # Get current face_id for selection state management
                    current_face_id = original_faces[i][0]  # Get current face_id
                    
                    # Reuse auto-match display logic for similar faces
                    self._display_similar_faces_in_panel(similar_scrollable_frame, unidentified_similar_faces, 
                                                       similar_face_vars, similar_face_images, similar_face_crops,
                                                       current_face_id, face_selection_states, identify_data_cache)
                    
                    # Note: Selection states are now restored automatically during checkbox creation (auto-match style)
                else:
                    # No similar unidentified faces found
                    no_faces_label = ttk.Label(similar_scrollable_frame, text="No similar unidentified faces found", 
                                             foreground="gray", font=("Arial", 10))
                    no_faces_label.pack(pady=20)
            else:
                # Compare disabled - clear the panel
                clear_label = ttk.Label(similar_scrollable_frame, text="Enable 'Compare with similar faces' to see matches", 
                                      foreground="gray", font=("Arial", 10))
                clear_label.pack(pady=20)
            
            # Update button states based on compare checkbox and list contents
            update_select_clear_buttons_state()
        
        # Unique faces change handler (must be defined before checkbox that uses it)
        def on_unique_faces_change():
            """Handle unique faces checkbox change"""
            nonlocal original_faces, i
            
            if unique_faces_var.get():
                # Show progress message
                print("🔄 Applying unique faces filter...")
                root.update()  # Update UI to show the message
                
                # Apply unique faces filtering to the main face list
                try:
                    original_faces = self._filter_unique_faces_from_list(original_faces)
                    print(f"✅ Filter applied: {len(original_faces)} unique faces remaining")
                except Exception as e:
                    print(f"⚠️  Error applying filter: {e}")
                    # Revert checkbox state
                    unique_faces_var.set(False)
                    return
            else:
                # Reload the original unfiltered face list
                print("🔄 Reloading all faces...")
                root.update()  # Update UI to show the message
                
                with self.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)
                    
                    query += ' ORDER BY f.id'
                    cursor.execute(query, params)
                    original_faces = list(cursor.fetchall())
                
                print(f"✅ Reloaded: {len(original_faces)} faces")
            
            # Reset to first face and update display
            i = 0
            update_similar_faces()

        # Compare checkbox variable and handler (must be defined before widgets that use it)
        compare_var = tk.BooleanVar()
        
        def on_compare_change():
            """Handle compare checkbox change"""
            update_similar_faces()
            update_select_clear_buttons_state()

        # Date filter controls
        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))
        date_from_var = tk.StringVar(value=date_from or "")
        date_from_entry = ttk.Entry(date_filter_frame, textvariable=date_from_var, width=10, state='readonly')
        date_from_entry.grid(row=0, column=1, sticky=tk.W, padx=(0, 5))
        
        # Calendar button for date from
        def open_calendar_from():
            open_date_calendar(date_from_var, "Select Start Date")
        
        calendar_from_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_from)
        calendar_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))
        date_to_var = tk.StringVar(value=date_to or "")
        date_to_entry = ttk.Entry(date_filter_frame, textvariable=date_to_var, width=10, state='readonly')
        date_to_entry.grid(row=0, column=4, sticky=tk.W, padx=(0, 5))
        
        # Calendar button for date to
        def open_calendar_to():
            open_date_calendar(date_to_var, "Select End Date")
        
        calendar_to_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_to)
        calendar_to_btn.grid(row=0, column=5, padx=(0, 10))
        
        # Apply filter button
        def apply_date_filter():
            nonlocal date_from, date_to
            date_from = date_from_var.get().strip() or None
            date_to = date_to_var.get().strip() or None
            date_processed_from = date_processed_from_var.get().strip() or None
            date_processed_to = date_processed_to_var.get().strip() or None
            
            # Reload faces with new date filter
            with self.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)
                unidentified = cursor.fetchall()
            
            if not unidentified:
                messagebox.showinfo("No Faces Found", "No unidentified faces found with the current date filters.")
                return
            
            # Update the global unidentified list and reset position
            nonlocal current_pos, total_unidentified
            current_pos = 0
            total_unidentified = len(unidentified)
            
            # Reset to first face - display will update when user navigates
            if len(unidentified) > 0:
                # Reset to first face
                current_pos = 0
                # The display will be updated when the user navigates or when the window is shown
            
            # Build filter description
            filters_applied = []
            if date_from or date_to:
                taken_filter = f"taken: {date_from or 'any'} to {date_to or 'any'}"
                filters_applied.append(taken_filter)
            if date_processed_from or date_processed_to:
                processed_filter = f"processed: {date_processed_from or 'any'} to {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(unidentified)} unidentified faces with date filters")
            print("💡 Navigate to refresh the display with filtered faces")
        
        # Apply filter button (inside filter frame)
        apply_filter_btn = ttk.Button(date_filter_frame, text="Apply Filter", command=apply_date_filter)
        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))
        date_processed_from_var = tk.StringVar()
        date_processed_from_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_from_var, width=10, state='readonly')
        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():
            open_date_calendar(date_processed_from_var, "Select Processing Start Date")
        
        calendar_processed_from_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_processed_from)
        calendar_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))
        date_processed_to_var = tk.StringVar()
        date_processed_to_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_to_var, width=10, state='readonly')
        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():
            open_date_calendar(date_processed_to_var, "Select Processing End Date")
        
        calendar_processed_to_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_processed_to)
        calendar_processed_to_btn.grid(row=1, column=5, padx=(0, 10), pady=(10, 0))
        
        # Unique checkbox under the filter frame
        unique_faces_checkbox = ttk.Checkbutton(main_frame, text="Unique faces only", 
                                              variable=unique_faces_var, command=on_unique_faces_change)
        unique_faces_checkbox.grid(row=2, column=0, sticky=tk.W, padx=(0, 5), pady=0)
        
        # Compare checkbox on the same row as Unique
        compare_checkbox = ttk.Checkbutton(main_frame, text="Compare similar faces", variable=compare_var, 
                                         command=on_compare_change)
        compare_checkbox.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
        right_panel = ttk.LabelFrame(main_frame, text="Similar Faces", padding="5")
        right_panel.grid(row=3, column=1, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
        right_panel.columnconfigure(0, weight=1)
        right_panel.rowconfigure(0, weight=1)  # Make right panel expandable vertically
        
        # 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'
        canvas = tk.Canvas(image_frame, width=400, height=400, bg=canvas_bg_color, highlightthickness=0)
        canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 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))
        first_name_var = tk.StringVar()
        first_name_entry = ttk.Entry(input_frame, textvariable=first_name_var, width=12)
        first_name_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 5))
        
        # Red asterisk for required first name field (overlayed, no layout impact)
        first_name_asterisk = ttk.Label(root, text="*", foreground="red")
        first_name_asterisk.place_forget()
        
        # Last name input (with live listbox autocomplete)
        ttk.Label(input_frame, text="Last name:").grid(row=0, column=2, sticky=tk.W, padx=(10, 10))
        last_name_var = tk.StringVar()
        last_name_entry = ttk.Entry(input_frame, textvariable=last_name_var, width=12)
        last_name_entry.grid(row=0, column=3, sticky=(tk.W, tk.E), padx=(0, 5))
        
        # Red asterisk for required last name field (overlayed, no layout impact)
        last_name_asterisk = ttk.Label(root, text="*", foreground="red")
        last_name_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()
                first_name_entry.update_idletasks()
                last_name_entry.update_idletasks()
                date_of_birth_entry.update_idletasks()
                
                # Get absolute coordinates relative to root window
                first_root_x = first_name_entry.winfo_rootx()
                first_root_y = first_name_entry.winfo_rooty()
                first_w = 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 = last_name_entry.winfo_rootx()
                last_root_y = last_name_entry.winfo_rooty()
                last_w = 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 = date_of_birth_entry.winfo_rootx()
                dob_root_y = date_of_birth_entry.winfo_rooty()
                dob_w = 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)
                first_name_entry.bind('<Configure>', _position_required_asterisks)
                last_name_entry.bind('<Configure>', _position_required_asterisks)
                date_of_birth_entry.bind('<Configure>', _position_required_asterisks)
                _position_required_asterisks()
            except Exception:
                pass
        root.after(100, _bind_asterisk_positioning)
        
        # 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
        
        def _show_suggestions():
            """Show filtered suggestions in listbox"""
            all_last_names = identify_data_cache.get('last_names', [])
            typed = 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 = last_name_entry.winfo_rootx()
                entry_root_y = last_name_entry.winfo_rooty()
                entry_height = 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 = 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])
                last_name_var.set(selected_name)
            _hide_suggestions()
            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)
                    last_name_var.set(selected_name)
            except:
                pass
            _hide_suggestions()
            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()
                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()
                    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
        last_name_entry.bind('<KeyRelease>', lambda e: _safe_show_suggestions())
        last_name_entry.bind('<KeyPress>', _on_key_press)
        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)
        
        # Middle name input
        ttk.Label(input_frame, text="Middle name:").grid(row=0, column=4, sticky=tk.W, padx=(10, 10))
        middle_name_var = tk.StringVar()
        middle_name_entry = ttk.Entry(input_frame, textvariable=middle_name_var, width=12)
        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))
        date_of_birth_var = tk.StringVar()
        
        # 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))
        maiden_name_var = tk.StringVar()
        maiden_name_entry = ttk.Entry(input_frame, textvariable=maiden_name_var, width=12)
        maiden_name_entry.grid(row=1, column=3, sticky=(tk.W, tk.E), padx=(0, 5))
        
        # Date display entry (read-only)
        date_of_birth_entry = ttk.Entry(date_frame, textvariable=date_of_birth_var, width=12, state='readonly')
        date_of_birth_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
        
        # Red asterisk for required date of birth field (overlayed, no layout impact)
        date_asterisk = ttk.Label(root, text="*", foreground="red")
        date_asterisk.place_forget()
        
        # Calendar button
        calendar_btn = ttk.Button(date_frame, text="📅", width=3, command=lambda: open_calendar())
        calendar_btn.pack(side=tk.RIGHT, padx=(15, 0))
        
        def open_calendar():
            """Open a visual calendar dialog to select date of birth"""
            from datetime import datetime, date, timedelta
            import calendar
            
            # Create calendar window
            calendar_window = tk.Toplevel(root)
            calendar_window.title("Select Date of Birth")
            calendar_window.resizable(False, False)
            calendar_window.transient(root)
            calendar_window.grab_set()
            
            # Calculate center position before showing the window
            window_width = 400
            window_height = 400
            screen_width = calendar_window.winfo_screenwidth()
            screen_height = calendar_window.winfo_screenheight()
            x = (screen_width // 2) - (window_width // 2)
            y = (screen_height // 2) - (window_height // 2)
            
            # Set geometry with center position before showing
            calendar_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
            
            # Calendar variables
            current_date = datetime.now()
            
            # Check if there's already a date selected
            existing_date_str = date_of_birth_var.get().strip()
            if existing_date_str:
                try:
                    existing_date = datetime.strptime(existing_date_str, '%Y-%m-%d').date()
                    display_year = existing_date.year
                    display_month = existing_date.month
                    selected_date = existing_date
                except ValueError:
                    # If existing date is invalid, use default
                    display_year = current_date.year - 25
                    display_month = 1
                    selected_date = None
            else:
                # Default to 25 years ago
                display_year = current_date.year - 25
                display_month = 1
                selected_date = None
            
            # Month names
            month_names = ["January", "February", "March", "April", "May", "June",
                          "July", "August", "September", "October", "November", "December"]
            
            # Configure custom styles for better visual highlighting
            style = ttk.Style()
            
            # Selected date style - bright blue background with white text
            style.configure("Selected.TButton", 
                          background="#0078d4", 
                          foreground="white",
                          font=("Arial", 9, "bold"),
                          relief="raised",
                          borderwidth=2)
            style.map("Selected.TButton",
                     background=[("active", "#106ebe")],
                     relief=[("pressed", "sunken")])
            
            # Today's date style - orange background
            style.configure("Today.TButton",
                          background="#ff8c00",
                          foreground="white", 
                          font=("Arial", 9, "bold"),
                          relief="raised",
                          borderwidth=1)
            style.map("Today.TButton",
                     background=[("active", "#e67e00")],
                     relief=[("pressed", "sunken")])
            
            # Calendar-specific normal button style (don't affect global TButton)
            style.configure("Calendar.TButton",
                          font=("Arial", 9),
                          relief="flat")
            style.map("Calendar.TButton",
                     background=[("active", "#e1e1e1")],
                     relief=[("pressed", "sunken")])
            
            # Main frame
            main_cal_frame = ttk.Frame(calendar_window, padding="10")
            main_cal_frame.pack(fill=tk.BOTH, expand=True)
            
            # Header frame with navigation
            header_frame = ttk.Frame(main_cal_frame)
            header_frame.pack(fill=tk.X, pady=(0, 10))
            
            # Month/Year display and navigation
            nav_frame = ttk.Frame(header_frame)
            nav_frame.pack()
            
            def update_calendar():
                """Update the calendar display"""
                # Clear existing calendar
                for widget in calendar_frame.winfo_children():
                    widget.destroy()
                
                # Update header
                month_year_label.config(text=f"{month_names[display_month-1]} {display_year}")
                
                # Get calendar data
                cal = calendar.monthcalendar(display_year, display_month)
                
                # Day headers
                day_headers = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
                for i, day in enumerate(day_headers):
                    label = ttk.Label(calendar_frame, text=day, font=("Arial", 9, "bold"))
                    label.grid(row=0, column=i, padx=1, pady=1, sticky="nsew")
                
                # Calendar days
                for week_num, week in enumerate(cal):
                    for day_num, day in enumerate(week):
                        if day == 0:
                            # Empty cell
                            label = ttk.Label(calendar_frame, text="")
                            label.grid(row=week_num+1, column=day_num, padx=1, pady=1, sticky="nsew")
                        else:
                            # Day button
                            def make_day_handler(day_value):
                                def select_day():
                                    nonlocal selected_date
                                    selected_date = date(display_year, display_month, day_value)
                                    # Reset all buttons to normal calendar style
                                    for widget in calendar_frame.winfo_children():
                                        if isinstance(widget, ttk.Button):
                                            widget.config(style="Calendar.TButton")
                                    # Highlight selected day with prominent style
                                    for widget in calendar_frame.winfo_children():
                                        if isinstance(widget, ttk.Button) and widget.cget("text") == str(day_value):
                                            widget.config(style="Selected.TButton")
                                return select_day
                            
                            day_btn = ttk.Button(calendar_frame, text=str(day), 
                                               command=make_day_handler(day),
                                               width=3, style="Calendar.TButton")
                            day_btn.grid(row=week_num+1, column=day_num, padx=1, pady=1, sticky="nsew")
                            
                            # Check if this day should be highlighted
                            is_today = (display_year == current_date.year and 
                                       display_month == current_date.month and 
                                       day == current_date.day)
                            is_selected = (selected_date and 
                                         selected_date.year == display_year and 
                                         selected_date.month == display_month and 
                                         selected_date.day == day)
                            
                            if is_selected:
                                day_btn.config(style="Selected.TButton")
                            elif is_today:
                                day_btn.config(style="Today.TButton")
            
            # Navigation functions
            def prev_year():
                nonlocal display_year
                display_year = max(1900, display_year - 1)
                update_calendar()
            
            def next_year():
                nonlocal display_year
                display_year = min(current_date.year, display_year + 1)
                update_calendar()
            
            def prev_month():
                nonlocal display_month, display_year
                if display_month > 1:
                    display_month -= 1
                else:
                    display_month = 12
                    display_year = max(1900, display_year - 1)
                update_calendar()
            
            def next_month():
                nonlocal display_month, display_year
                if display_month < 12:
                    display_month += 1
                else:
                    display_month = 1
                    display_year = min(current_date.year, display_year + 1)
                update_calendar()
            
            # Navigation buttons
            prev_year_btn = ttk.Button(nav_frame, text="<<", width=3, command=prev_year)
            prev_year_btn.pack(side=tk.LEFT, padx=(0, 2))
            
            prev_month_btn = ttk.Button(nav_frame, text="<", width=3, command=prev_month)
            prev_month_btn.pack(side=tk.LEFT, padx=(0, 5))
            
            month_year_label = ttk.Label(nav_frame, text="", font=("Arial", 12, "bold"))
            month_year_label.pack(side=tk.LEFT, padx=5)
            
            next_month_btn = ttk.Button(nav_frame, text=">", width=3, command=next_month)
            next_month_btn.pack(side=tk.LEFT, padx=(5, 2))
            
            next_year_btn = ttk.Button(nav_frame, text=">>", width=3, command=next_year)
            next_year_btn.pack(side=tk.LEFT)
            
            # Calendar grid frame
            calendar_frame = ttk.Frame(main_cal_frame)
            calendar_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
            
            # Configure grid weights
            for i in range(7):
                calendar_frame.columnconfigure(i, weight=1)
            for i in range(7):
                calendar_frame.rowconfigure(i, weight=1)
            
            # Buttons frame
            buttons_frame = ttk.Frame(main_cal_frame)
            buttons_frame.pack(fill=tk.X)
            
            def select_date():
                """Select the date and close calendar"""
                if selected_date:
                    date_str = selected_date.strftime('%Y-%m-%d')
                    date_of_birth_var.set(date_str)
                    calendar_window.destroy()
                else:
                    messagebox.showwarning("No Date Selected", "Please select a date from the calendar.")
            
            def cancel_selection():
                """Cancel date selection"""
                calendar_window.destroy()
            
            # Buttons
            ttk.Button(buttons_frame, text="Select", command=select_date).pack(side=tk.LEFT, padx=(0, 5))
            ttk.Button(buttons_frame, text="Cancel", command=cancel_selection).pack(side=tk.LEFT)
            
            # Initialize calendar
            update_calendar()
        
        # (moved) unique_faces_var is defined earlier before date filter widgets
        
        # (moved) update_similar_faces function is defined earlier before on_unique_faces_change
        
        # (moved) Compare checkbox is now inside date_filter_frame to the right of dates
        
        # (moved) on_unique_faces_change function is defined earlier before date filter widgets
        
        
        # Add callback to save person name when it changes
        def on_name_change(*args):
            if i < len(original_faces):
                current_face_id = original_faces[i][0]
                first_name = first_name_var.get().strip()
                last_name = last_name_var.get().strip()
                middle_name = middle_name_var.get().strip()
                maiden_name = maiden_name_var.get().strip()
                date_of_birth = date_of_birth_var.get().strip()
                
                if first_name or last_name or date_of_birth:
                    # Store as dictionary to maintain consistency
                    face_person_names[current_face_id] = {
                        'first_name': first_name,
                        'last_name': last_name,
                        'middle_name': middle_name,
                        'maiden_name': maiden_name,
                        'date_of_birth': date_of_birth
                    }
                elif current_face_id in face_person_names:
                    # Remove empty names from storage
                    del face_person_names[current_face_id]
        
        first_name_var.trace('w', on_name_change)
        last_name_var.trace('w', on_name_change)
        date_of_birth_var.trace('w', on_name_change)
        
        # Buttons moved to bottom of window
        
        
        # Right panel for similar faces
        similar_faces_frame = ttk.Frame(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"""
            for face_id, var in similar_face_vars:
                var.set(True)
        
        def clear_all_similar_faces():
            """Clear all similar faces checkboxes"""
            for face_id, var in similar_face_vars:
                var.set(False)
        
        select_all_btn = ttk.Button(similar_controls_frame, text="☑️ Select All", command=select_all_similar_faces, state='disabled')
        select_all_btn.pack(side=tk.LEFT, padx=(0, 5))
        
        clear_all_btn = ttk.Button(similar_controls_frame, text="☐ Clear All", command=clear_all_similar_faces, state='disabled')
        clear_all_btn.pack(side=tk.LEFT)
        
        def update_select_clear_buttons_state():
            """Enable/disable Select All and Clear All based on compare state and presence of items"""
            if compare_var.get() and similar_face_vars:
                select_all_btn.config(state='normal')
                clear_all_btn.config(state='normal')
            else:
                select_all_btn.config(state='disabled')
                clear_all_btn.config(state='disabled')
        
        # Create canvas for similar faces with scrollbar
        style = ttk.Style()
        canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
        similar_canvas = tk.Canvas(similar_faces_frame, bg=canvas_bg_color, highlightthickness=0)
        similar_scrollbar = ttk.Scrollbar(similar_faces_frame, orient="vertical", command=similar_canvas.yview)
        similar_scrollable_frame = ttk.Frame(similar_canvas)
        
        similar_scrollable_frame.bind(
            "<Configure>",
            lambda e: similar_canvas.configure(scrollregion=similar_canvas.bbox("all"))
        )
        
        similar_canvas.create_window((0, 0), window=similar_scrollable_frame, anchor="nw")
        similar_canvas.configure(yscrollcommand=similar_scrollbar.set)
        
        # Pack canvas and scrollbar
        similar_canvas.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        similar_scrollbar.grid(row=0, column=1, rowspan=2, sticky=(tk.N, tk.S))
        
        # Variables for similar faces
        similar_faces_data = []
        similar_face_vars = []
        similar_face_images = []
        similar_face_crops = []
        
        # Store face selection states per face ID to preserve selections during navigation (auto-match style)
        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}
        
        def save_current_face_selection_states():
            """Save current checkbox states and person name for the current face (auto-match style backup)"""
            if i < len(original_faces):
                current_face_id = original_faces[i][0]
                
                # Save checkbox states
                if similar_face_vars:
                    if current_face_id not in face_selection_states:
                        face_selection_states[current_face_id] = {}
                    
                    # Save current checkbox states using unique keys
                    for similar_face_id, var in similar_face_vars:
                        unique_key = f"{current_face_id}_{similar_face_id}"
                        face_selection_states[current_face_id][unique_key] = var.get()
                
                # Save person name and date of birth
                first_name = first_name_var.get().strip()
                last_name = last_name_var.get().strip()
                middle_name = middle_name_var.get().strip()
                maiden_name = maiden_name_var.get().strip()
                date_of_birth = date_of_birth_var.get().strip()
                
                if first_name or last_name:
                    # Store all fields
                    face_person_names[current_face_id] = {
                        'first_name': first_name,
                        'last_name': last_name,
                        'middle_name': middle_name,
                        'maiden_name': maiden_name,
                        'date_of_birth': date_of_birth
                    }
        
        # Button commands
        command = None
        waiting_for_input = False
        
        def on_identify():
            nonlocal command, waiting_for_input
            first_name = first_name_var.get().strip()
            last_name = last_name_var.get().strip()
            middle_name = middle_name_var.get().strip()
            maiden_name = maiden_name_var.get().strip()
            date_of_birth = date_of_birth_var.get().strip()
            compare_enabled = compare_var.get()
            
            if not first_name:
                print("⚠️  Please enter a first name before identifying")
                return
            
            if not last_name:
                print("⚠️  Please enter a last name before identifying")
                return
            
            if not date_of_birth:
                print("⚠️  Please select a date of birth before identifying")
                return
            
            # Validate date format (YYYY-MM-DD) - should always be valid from calendar
            try:
                from datetime import datetime
                datetime.strptime(date_of_birth, '%Y-%m-%d')
            except ValueError:
                print("⚠️  Invalid date format. Please use the calendar to select a date.")
                return
            
            # Combine first and last name properly
            if last_name and first_name:
                command = f"{last_name}, {first_name}"
            elif last_name:
                command = last_name
            elif first_name:
                command = first_name
            else:
                command = ""
            
            # Store the additional fields for database insertion
            # We'll pass them through the command structure
            if middle_name or maiden_name:
                command += f"|{middle_name}|{maiden_name}|{date_of_birth}"
            else:
                command += f"|||{date_of_birth}"
            
            if not command:
                print("⚠️  Please enter at least a first name or last name before identifying")
                return
            
            if compare_enabled:
                # Get selected similar faces
                selected_face_ids = [face_id for face_id, var in similar_face_vars if var.get()]
                if selected_face_ids:
                    # Create compare command with selected face IDs
                    command = f"compare:{command}:{','.join(map(str, selected_face_ids))}"
                # If no similar faces selected, just identify the current face
            else:
                # Regular identification
                pass
            
            waiting_for_input = False
        
        
        def validate_navigation():
            """Check if navigation is allowed (no selected similar faces without person name)"""
            # Check if compare is enabled and similar faces are selected
            if compare_var.get() and similar_face_vars:
                selected_faces = [face_id for face_id, var in similar_face_vars if var.get()]
                first_name = first_name_var.get().strip()
                last_name = last_name_var.get().strip()
                if selected_faces and not (first_name or last_name):
                    # Show warning dialog
                    result = messagebox.askyesno(
                        "Selected Faces Not Identified",
                        f"You have {len(selected_faces)} similar face(s) selected but no person name entered.\n\n"
                        "These faces will not be identified if you continue.\n\n"
                        "Do you want to continue anyway?",
                        icon='warning'
                    )
                    return result  # True = continue, False = cancel
            return True  # No validation issues, allow navigation
        
        def on_back():
            nonlocal command, waiting_for_input
            if not validate_navigation():
                return  # Cancel navigation
            command = 'back'
            waiting_for_input = False
        
        def on_skip():
            nonlocal command, waiting_for_input
            if not validate_navigation():
                return  # Cancel navigation
            command = 's'
            waiting_for_input = False
        
        def on_quit():
            nonlocal command, waiting_for_input, window_destroyed, force_exit
            
            # First check for selected similar faces without person name
            if not validate_navigation():
                return  # Cancel quit
            
            # Check if there are pending identifications (faces with complete data but not yet saved)
            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
                    else:
                        # Handle legacy string format
                        person_name = v.strip()
                        date_of_birth = ''  # Legacy format doesn't have date_of_birth
                        # Legacy format is not considered complete without date of birth
                        pass
            
            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 quitting?\n\n"
                    "• Yes: Save all pending identifications and quit\n"
                    "• No: Quit without saving\n"
                    "• Cancel: Return to identification"
                )
                
                if result is True:  # Yes - Save and quit
                    save_all_pending_identifications()
                    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
            else:
                # No pending identifications, quit normally
                command = 'q'
                waiting_for_input = False
            
            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()
        
        
        
        def update_button_states():
            """Update button states based on current position and unidentified faces"""
            # Check if there are previous unidentified faces
            has_prev_unidentified = 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':
                    has_prev_unidentified = True
                    break
            
            # Check if there are next unidentified faces
            has_next_unidentified = 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':
                    has_next_unidentified = True
                    break
            
            # Enable/disable Back button
            if has_prev_unidentified:
                back_btn.config(state='normal')
            else:
                back_btn.config(state='disabled')
            
            # Enable/disable Next button
            if has_next_unidentified:
                next_btn.config(state='normal')
            else:
                next_btn.config(state='disabled')
        
        # Button references moved to bottom control panel
        
        def update_identify_button_state():
            """Enable/disable identify button based on first name, last name, and date of birth"""
            first_name = first_name_var.get().strip()
            last_name = last_name_var.get().strip()
            date_of_birth = date_of_birth_var.get().strip()
            if first_name and last_name and date_of_birth:
                identify_btn.config(state='normal')
            else:
                identify_btn.config(state='disabled')
        
        # Bind name input changes to update button state
        first_name_var.trace('w', lambda *args: update_identify_button_state())
        last_name_var.trace('w', lambda *args: update_identify_button_state())
        date_of_birth_var.trace('w', lambda *args: update_identify_button_state())
        
        # Handle Enter key
        def on_enter(event):
            on_identify()
        
        first_name_entry.bind('<Return>', on_enter)
        last_name_entry.bind('<Return>', on_enter)
        
        # 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
        back_btn = ttk.Button(control_frame, text="⬅️ Back", command=on_back)
        next_btn = ttk.Button(control_frame, text="➡️ Next", command=on_skip)
        quit_btn = ttk.Button(control_frame, text="❌ Quit", command=on_quit)
        
        back_btn.pack(side=tk.LEFT, padx=(0, 5))
        next_btn.pack(side=tk.LEFT, padx=(0, 5))
        quit_btn.pack(side=tk.LEFT, padx=(5, 0))

        # Identify button (placed after on_identify is defined)
        identify_btn = ttk.Button(input_frame, text="✅ Identify", command=on_identify, state='disabled')
        identify_btn.grid(row=4, column=0, pady=(10, 0), sticky=tk.W)
        
        # Show the window
        try:
            root.deiconify()
            root.lift()
            root.focus_force()
        except tk.TclError:
            # Window was destroyed before we could show it
            conn.close()
            return 0
        
        
        
        # 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
        
        def get_unidentified_faces():
            """Get list of faces that haven't been identified yet"""
            return [face for face in original_faces if face[0] not in face_status or face_status[face[0]] != 'identified']
        
        def get_current_face_position():
            """Get current face position among unidentified faces"""
            unidentified_faces = get_unidentified_faces()
            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_current_face_index():
            """Update the current face index to point to a valid unidentified face"""
            nonlocal i
            unidentified_faces = get_unidentified_faces()
            if not unidentified_faces:
                # All faces identified, we're done
                return False
            
            # 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
        
        while not window_destroyed:
            # Check if current face is identified and update index if needed
            if not update_current_face_index():
                # 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 = get_current_face_position()
            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
            update_button_states()
            
            # Update similar faces panel if compare is enabled
            if compare_var.get():
                update_similar_faces()
            
            # Update photo info
            if is_already_identified:
                # Get the person name for this face
                with self.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:
                            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"
                
                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._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
            canvas.delete("all")
            if show_faces and face_crop_path and os.path.exists(face_crop_path):
                try:
                    # Load and display the face crop image
                    pil_image = Image.open(face_crop_path)
                    
                    # Get canvas dimensions
                    canvas_width = canvas.winfo_width()
                    canvas_height = canvas.winfo_height()
                    
                    # If canvas hasn't been rendered yet, force update and use actual size
                    if canvas_width <= 1 or canvas_height <= 1:
                        # Force the canvas to update its geometry
                        canvas.update_idletasks()
                        canvas_width = canvas.winfo_width()
                        canvas_height = canvas.winfo_height()
                        
                        # If still not rendered, use default size
                        if canvas_width <= 1:
                            canvas_width = 400
                        if canvas_height <= 1:
                            canvas_height = 400
                    
                    # Calculate scaling to fit within the canvas while maintaining aspect ratio
                    img_width, img_height = pil_image.size
                    scale_x = canvas_width / img_width
                    scale_y = canvas_height / img_height
                    # Allow slight upscaling (up to 1.2x) for better visibility, but cap to avoid excessive blurriness
                    max_scale = min(1.2, max(scale_x, scale_y))
                    scale = min(scale_x, scale_y, max_scale)
                    
                    # Resize image to fill canvas
                    new_width = int(img_width * scale)
                    new_height = int(img_height * scale)
                    pil_image = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
                    
                    photo = ImageTk.PhotoImage(pil_image)
                    
                    # Center the image in the canvas
                    x = canvas_width // 2
                    y = canvas_height // 2
                    canvas.create_image(x, y, image=photo)
                    
                    # Keep a reference to prevent garbage collection
                    canvas.image = photo
                    
                    # Add photo icon using reusable function
                    self._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)
                    
                except Exception as e:
                    canvas.create_text(200, 200, text=f"❌ Could not load image: {e}", fill="red")
            else:
                canvas.create_text(200, 200, text="🖼️ No face crop available", fill="gray")
            
            # Set person name input - restore saved name or use database/empty value
            if face_id in face_person_names:
                # Restore previously entered name for this face
                person_data = face_person_names[face_id]
                if isinstance(person_data, dict):
                    # Handle dictionary format - use individual field values for proper restoration
                    first_name = person_data.get('first_name', '').strip()
                    last_name = person_data.get('last_name', '').strip()
                    middle_name = person_data.get('middle_name', '').strip()
                    maiden_name = person_data.get('maiden_name', '').strip()
                    date_of_birth = person_data.get('date_of_birth', '').strip()
                    
                    # Restore all fields directly
                    first_name_var.set(first_name)
                    last_name_var.set(last_name)
                    middle_name_var.set(middle_name)
                    maiden_name_var.set(maiden_name)
                    date_of_birth_var.set(date_of_birth)
                else:
                    # Handle legacy string format (for backward compatibility)
                    full_name = person_data
                    # Parse "Last, First" format back to separate fields
                    if ', ' in full_name:
                        parts = full_name.split(', ', 1)
                        last_name_var.set(parts[0].strip())
                        first_name_var.set(parts[1].strip())
                    else:
                        # Single name format
                        first_name_var.set(full_name)
                        last_name_var.set("")
            elif is_already_identified:
                # Pre-populate with the current person name from database
                with self.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_var.set(result[0] or "")
                        last_name_var.set(result[1] or "")
                        middle_name_var.set(result[2] or "")
                        maiden_name_var.set(result[3] or "")
                        date_of_birth_var.set(result[4] or "")
                    else:
                        first_name_var.set("")
                        last_name_var.set("")
                        middle_name_var.set("")
                        maiden_name_var.set("")
                        date_of_birth_var.set("")
            else:
                first_name_var.set("")
                last_name_var.set("")
                middle_name_var.set("")
                maiden_name_var.set("")
                date_of_birth_var.set("")
            
            # Keep compare checkbox state persistent across navigation
            first_name_entry.focus_set()
            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()
                    # 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._cleanup_face_crops(face_crop_path)
                self.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':
                print("Quitting...")
                # Clean up face crops and caches
                self._cleanup_face_crops(face_crop_path)
                self.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 (auto-match style backup)
                save_current_face_selection_states()
                
                # 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
                date_of_birth_var.set("")
                # Clear middle name and maiden name fields when moving to next face
                middle_name_var.set("")
                maiden_name_var.set("")
                
                update_button_states()
                # Only update similar faces if compare is enabled
                if compare_var.get():
                    update_similar_faces()
                continue
            
            elif command.lower() == 'back':
                print("⬅️  Going back to previous face")
                
                # Save current checkbox states before navigating away (auto-match style backup)
                save_current_face_selection_states()
                
                # 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
                current_face_id = original_faces[i][0]
                if current_face_id in face_person_names:
                    person_data = face_person_names[current_face_id]
                    if isinstance(person_data, dict):
                        # Use individual field values for proper restoration
                        first_name = person_data.get('first_name', '').strip()
                        last_name = person_data.get('last_name', '').strip()
                        middle_name = person_data.get('middle_name', '').strip()
                        maiden_name = person_data.get('maiden_name', '').strip()
                        date_of_birth = person_data.get('date_of_birth', '').strip()
                        
                        # Restore all fields directly
                        first_name_var.set(first_name)
                        last_name_var.set(last_name)
                        middle_name_var.set(middle_name)
                        maiden_name_var.set(maiden_name)
                        date_of_birth_var.set(date_of_birth)
                    else:
                        # Clear fields
                        first_name_var.set("")
                        last_name_var.set("")
                        middle_name_var.set("")
                        maiden_name_var.set("")
                        date_of_birth_var.set("")
                else:
                    # No saved data - clear fields
                    first_name_var.set("")
                    last_name_var.set("")
                    middle_name_var.set("")
                    maiden_name_var.set("")
                    date_of_birth_var.set("")
                
                update_button_states()
                # Only update similar faces if compare is enabled
                if compare_var.get():
                    update_similar_faces()
                continue
            
            elif command.lower() == 'list':
                self._show_people_list()
                continue
            
            elif command:
                try:
                    # Check if this is a compare command
                    if command.startswith('compare:'):
                        # Parse compare command: compare:person_name:face_id1,face_id2,face_id3
                        parts = command.split(':', 2)
                        if len(parts) == 3:
                            person_name = parts[1]
                            selected_face_ids = [int(fid.strip()) for fid in parts[2].split(',') if fid.strip()]
                            
                            with self.get_db_connection() as conn:
                                cursor = conn.cursor()
                                # Add person if doesn't exist
                                # Parse person_name in "Last, First" or single-token format
                                # Parse person_name with additional fields (middle_name|maiden_name|date_of_birth)
                                name_part, middle_name, maiden_name, date_of_birth = person_name.split('|', 3)
                                parts = [p.strip() for p in name_part.split(',', 1)]
                                
                                if len(parts) == 2:
                                    last_name, first_name = parts[0], parts[1]
                                else:
                                    first_name = parts[0] if parts else ''
                                    last_name = ''
                                
                                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
                                if person_name not in identify_data_cache['people_names']:
                                    identify_data_cache['people_names'].append(person_name)
                                    identify_data_cache['people_names'].sort()  # Keep sorted
                                # Update last names cache from person_name ("Last, First" or single)
                                inferred_last = person_name.split(',')[0].strip() if ',' in person_name else person_name.strip()
                                if inferred_last:
                                    if 'last_names' not in identify_data_cache:
                                        identify_data_cache['last_names'] = []
                                    if inferred_last not in identify_data_cache['last_names']:
                                        identify_data_cache['last_names'].append(inferred_last)
                                        identify_data_cache['last_names'].sort()
                                
                                # Identify all selected faces (including current face)
                                all_face_ids = [face_id] + selected_face_ids
                                for fid in all_face_ids:
                                    cursor.execute(
                                        'UPDATE faces SET person_id = ? WHERE id = ?',
                                        (person_id, fid)
                                    )
                                
                                # Mark all faces as identified in our tracking
                                for fid in all_face_ids:
                                    face_status[fid] = 'identified'
                            
                            if is_already_identified:
                                print(f"✅ Re-identified current face and {len(selected_face_ids)} similar faces as: {person_name}")
                            else:
                                print(f"✅ Identified current face and {len(selected_face_ids)} similar faces as: {person_name}")
                                identified_count += 1 + len(selected_face_ids)
                            
                            
                            # Update person encodings after database transaction is complete
                            self._update_person_encodings(person_id)
                        else:
                            print("❌ Invalid compare command format")
                    else:
                        # Regular identification
                        with self.get_db_connection() as conn:
                            cursor = conn.cursor()
                            # Add person if doesn't exist
                            # Parse command in "Last, First" or single-token format
                            # Parse command with additional fields (middle_name|maiden_name|date_of_birth)
                            name_part, middle_name, maiden_name, date_of_birth = command.split('|', 3)
                            parts = [p.strip() for p in name_part.split(',', 1)]
                            
                            if len(parts) == 2:
                                last_name, first_name = parts[0], parts[1]
                            else:
                                first_name = parts[0] if parts else ''
                                last_name = ''
                            
                            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
                            if command not in identify_data_cache['people_names']:
                                identify_data_cache['people_names'].append(command)
                                identify_data_cache['people_names'].sort()  # Keep sorted
                            # Update last names cache from command ("Last, First" or single)
                            inferred_last = command.split(',')[0].strip() if ',' in command else command.strip()
                            if inferred_last:
                                if 'last_names' not in identify_data_cache:
                                    identify_data_cache['last_names'] = []
                                if inferred_last not in identify_data_cache['last_names']:
                                    identify_data_cache['last_names'].append(inferred_last)
                                    identify_data_cache['last_names'].sort()
                            
                            # Assign face to person
                            cursor.execute(
                                'UPDATE faces SET person_id = ? WHERE id = ?',
                                (person_id, face_id)
                            )
                        
                        if is_already_identified:
                            print(f"✅ Re-identified as: {command}")
                        else:
                            print(f"✅ Identified as: {command}")
                            identified_count += 1
                        
                        # Mark this face as identified in our tracking
                        face_status[face_id] = 'identified'
                        
                        
                        # Update person encodings after database transaction is complete
                        self._update_person_encodings(person_id)
                    
                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
                update_button_states()
                # Only update similar faces if compare is enabled
                if compare_var.get():
                    update_similar_faces()
                
                # 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 _display_similar_faces_in_panel(self, parent_frame, similar_faces_data, face_vars, face_images, face_crops, current_face_id=None, face_selection_states=None, data_cache=None):
        """Display similar faces in a panel - reuses auto-match display logic"""
        import tkinter as tk
        from tkinter import ttk
        from PIL import Image, ImageTk
        import os
        
        # Create all similar faces using auto-match style display
        for i, face_data in enumerate(similar_faces_data[:10]):  # Limit to 10 faces
            similar_face_id = face_data['face_id']
            filename = face_data['filename']
            distance = face_data['distance']
            quality = face_data.get('quality_score', 0.5)
            
            # Calculate confidence like in auto-match
            confidence_pct = (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()
            face_vars.append((similar_face_id, match_var))
            
            # Restore previous checkbox state if available (auto-match style)
            if current_face_id is not None and face_selection_states is not None:
                unique_key = f"{current_face_id}_{similar_face_id}"
                if current_face_id in face_selection_states and unique_key in face_selection_states[current_face_id]:
                    saved_state = face_selection_states[current_face_id][unique_key]
                    match_var.set(saved_state)
                
                # Add immediate callback to save state when checkbox changes (auto-match style)
                def make_callback(var, face_id, similar_face_id):
                    def on_checkbox_change(*args):
                        unique_key = f"{face_id}_{similar_face_id}"
                        if face_id not in face_selection_states:
                            face_selection_states[face_id] = {}
                        face_selection_states[face_id][unique_key] = var.get()
                    return on_checkbox_change
                
                # Bind the callback to the variable
                match_var.trace('w', make_callback(match_var, current_face_id, similar_face_id))
            
            # Configure match frame for grid layout
            match_frame.columnconfigure(0, weight=0)  # Checkbox column - fixed width
            match_frame.columnconfigure(1, weight=1)  # Text column - expandable
            match_frame.columnconfigure(2, weight=0)  # Image column - fixed width
            
            # Checkbox without text
            checkbox = ttk.Checkbutton(match_frame, variable=match_var)
            checkbox.grid(row=0, column=0, rowspan=2, sticky=(tk.W, tk.N), padx=(0, 5))
            
            # Create labels for confidence and filename
            confidence_label = ttk.Label(match_frame, text=f"{confidence_pct:.1f}% {confidence_desc}", font=("Arial", 9, "bold"))
            confidence_label.grid(row=0, column=1, sticky=tk.W, padx=(0, 10))
            
            filename_label = ttk.Label(match_frame, text=f"📁 {filename}", font=("Arial", 8), foreground="gray")
            filename_label.grid(row=1, column=1, sticky=tk.W, padx=(0, 10))
            
            # Face image (reusing auto-match image display)
            try:
                # Get photo path from cache or database
                photo_path = None
                if data_cache and 'photo_paths' in data_cache:
                    # Find photo path by filename in cache
                    for photo_data in data_cache['photo_paths'].values():
                        if photo_data['filename'] == filename:
                            photo_path = photo_data['path']
                            break
                
                # Fallback to database if not in cache
                if photo_path is None:
                    with self.get_db_connection() as conn:
                        cursor = conn.cursor()
                        cursor.execute('SELECT path FROM photos WHERE filename = ?', (filename,))
                        result = cursor.fetchone()
                        photo_path = result[0] if result else None
                
                # Extract face crop using existing method
                face_crop_path = self._extract_face_crop(photo_path, face_data['location'], similar_face_id)
                if face_crop_path and os.path.exists(face_crop_path):
                    face_crops.append(face_crop_path)
                    
                    # Create canvas for face image (like in auto-match)
                    style = ttk.Style()
                    canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
                    match_canvas = tk.Canvas(match_frame, width=80, height=80, bg=canvas_bg_color, highlightthickness=0)
                    match_canvas.grid(row=0, column=2, rowspan=2, sticky=(tk.W, tk.N), padx=(10, 0))
                    
                    # Load and display image (reusing auto-match image loading)
                    pil_image = Image.open(face_crop_path)
                    pil_image.thumbnail((80, 80), Image.Resampling.LANCZOS)
                    photo = ImageTk.PhotoImage(pil_image)
                    match_canvas.create_image(40, 40, image=photo)
                    match_canvas.image = photo  # Keep reference
                    face_images.append(photo)
                    
                    # Add photo icon to the similar face
                    self._create_photo_icon(match_canvas, photo_path, icon_size=15,
                                          face_x=40, face_y=40, 
                                          face_width=80, face_height=80,
                                          canvas_width=80, canvas_height=80)
                else:
                    # No image available
                    match_canvas = tk.Canvas(match_frame, width=80, height=80, bg='white')
                    match_canvas.pack(side=tk.LEFT, padx=(10, 0))
                    match_canvas.create_text(40, 40, text="🖼️", fill="gray")
            except Exception as e:
                # Error loading image
                match_canvas = tk.Canvas(match_frame, width=80, height=80, bg='white')
                match_canvas.pack(side=tk.LEFT, padx=(10, 0))
                match_canvas.create_text(40, 40, text="❌", fill="red")
    
    def _create_photo_icon(self, canvas, photo_path, icon_size=20, icon_x=None, icon_y=None, 
                          canvas_width=None, canvas_height=None, face_x=None, face_y=None, 
                          face_width=None, face_height=None):
        """Create a reusable photo icon with tooltip on a canvas"""
        import tkinter as tk
        import subprocess
        import platform
        import os
        
        def open_source_photo(event):
            """Open the source photo in a properly sized window"""
            try:
                system = platform.system()
                if system == "Windows":
                    # Try to open with a specific image viewer that supports window sizing
                    try:
                        subprocess.run(["mspaint", photo_path], check=False)
                    except:
                        os.startfile(photo_path)
                elif system == "Darwin":  # macOS
                    # Use Preview with specific window size
                    subprocess.run(["open", "-a", "Preview", photo_path])
                else:  # Linux and others
                    # Try common image viewers with window sizing options
                    viewers_to_try = [
                        ["eog", "--new-window", photo_path],  # Eye of GNOME
                        ["gwenview", photo_path],  # KDE image viewer
                        ["feh", "--geometry", "800x600", photo_path],  # feh with specific size
                        ["gimp", photo_path],  # GIMP
                        ["xdg-open", photo_path]  # Fallback to default
                    ]
                    
                    opened = False
                    for viewer_cmd in viewers_to_try:
                        try:
                            result = subprocess.run(viewer_cmd, check=False, capture_output=True)
                            if result.returncode == 0:
                                opened = True
                                break
                        except:
                            continue
                    
                    if not opened:
                        # Final fallback
                        subprocess.run(["xdg-open", photo_path])
            except Exception as e:
                print(f"❌ Could not open photo: {e}")
        
        # Create tooltip for the icon
        tooltip = None
        
        def show_tooltip(event):
            nonlocal tooltip
            if tooltip:
                tooltip.destroy()
            tooltip = tk.Toplevel()
            tooltip.wm_overrideredirect(True)
            tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}")
            label = tk.Label(tooltip, text="Show original photo", 
                           background="lightyellow", relief="solid", borderwidth=1,
                           font=("Arial", 9))
            label.pack()
        
        def hide_tooltip(event):
            nonlocal tooltip
            if tooltip:
                tooltip.destroy()
                tooltip = None
        
        # Calculate icon position
        if icon_x is None or icon_y is None:
            if face_x is not None and face_y is not None and face_width is not None and face_height is not None:
                # Position relative to face image - exactly in the corner
                face_right = face_x + face_width // 2
                face_top = face_y - face_height // 2
                icon_x = face_right - icon_size
                icon_y = face_top
            else:
                # Position relative to canvas - exactly in the corner
                if canvas_width is None:
                    canvas_width = canvas.winfo_width()
                if canvas_height is None:
                    canvas_height = canvas.winfo_height()
                icon_x = canvas_width - icon_size
                icon_y = 0
        
        # Ensure icon stays within canvas bounds
        if canvas_width is None:
            canvas_width = canvas.winfo_width()
        if canvas_height is None:
            canvas_height = canvas.winfo_height()
        icon_x = min(icon_x, canvas_width - icon_size)
        icon_y = max(icon_y, 0)
        
        # Draw the photo icon
        canvas.create_rectangle(icon_x, icon_y, icon_x + icon_size, icon_y + icon_size, 
                              fill="white", outline="black", width=1, tags="photo_icon")
        canvas.create_text(icon_x + icon_size//2, icon_y + icon_size//2, 
                         text="📷", font=("Arial", 10), tags="photo_icon")
        
        # Bind events
        canvas.tag_bind("photo_icon", "<Button-1>", open_source_photo)
        canvas.tag_bind("photo_icon", "<Enter>", lambda e: (canvas.config(cursor="hand2"), show_tooltip(e)))
        canvas.tag_bind("photo_icon", "<Leave>", lambda e: (canvas.config(cursor=""), hide_tooltip(e)))
        canvas.tag_bind("photo_icon", "<Motion>", lambda e: (show_tooltip(e) if tooltip else None))
        
        return tooltip  # Return tooltip reference for cleanup if needed

    def _extract_face_crop(self, photo_path: str, location: tuple, face_id: int) -> str:
        """Extract and save individual face crop for identification with caching"""
        try:
            # Check cache first
            cache_key = f"{photo_path}_{location}_{face_id}"
            if cache_key in self._image_cache:
                cached_path = self._image_cache[cache_key]
                # Verify the cached file still exists
                if os.path.exists(cached_path):
                    return cached_path
                else:
                    # Remove from cache if file doesn't exist
                    del self._image_cache[cache_key]
            
            # Parse location tuple from string format
            if isinstance(location, str):
                location = eval(location)
            
            top, right, bottom, left = location
            
            # Load the image
            image = Image.open(photo_path)
            
            # Add padding around the face (20% of face size)
            face_width = right - left
            face_height = bottom - top
            padding_x = int(face_width * 0.2)
            padding_y = int(face_height * 0.2)
            
            # Calculate crop bounds with padding
            crop_left = max(0, left - padding_x)
            crop_top = max(0, top - padding_y)
            crop_right = min(image.width, right + padding_x)
            crop_bottom = min(image.height, bottom + padding_y)
            
            # Crop the face
            face_crop = image.crop((crop_left, crop_top, crop_right, crop_bottom))
            
            # Create temporary file for the face crop
            temp_dir = tempfile.gettempdir()
            face_filename = f"face_{face_id}_crop.jpg"
            face_path = os.path.join(temp_dir, face_filename)
            
            # Resize for better viewing (minimum 200px width)
            if face_crop.width < 200:
                ratio = 200 / face_crop.width
                new_width = 200
                new_height = int(face_crop.height * ratio)
                face_crop = face_crop.resize((new_width, new_height), Image.Resampling.LANCZOS)
            
            face_crop.save(face_path, "JPEG", quality=95)
            
            # Cache the result
            self._image_cache[cache_key] = face_path
            return face_path
            
        except Exception as e:
            if self.verbose >= 1:
                print(f"⚠️  Could not extract face crop: {e}")
            return None

    def _create_comparison_image(self, unid_crop_path: str, match_crop_path: str, person_name: str, confidence: float) -> str:
        """Create a side-by-side comparison image"""
        try:
            # Load both face crops
            unid_img = Image.open(unid_crop_path)
            match_img = Image.open(match_crop_path)
            
            # Resize both to same height for better comparison
            target_height = 300
            unid_ratio = target_height / unid_img.height
            match_ratio = target_height / match_img.height
            
            unid_resized = unid_img.resize((int(unid_img.width * unid_ratio), target_height), Image.Resampling.LANCZOS)
            match_resized = match_img.resize((int(match_img.width * match_ratio), target_height), Image.Resampling.LANCZOS)
            
            # Create comparison image
            total_width = unid_resized.width + match_resized.width + 20  # 20px gap
            comparison = Image.new('RGB', (total_width, target_height + 60), 'white')
            
            # Paste images
            comparison.paste(unid_resized, (0, 30))
            comparison.paste(match_resized, (unid_resized.width + 20, 30))
            
            # Add labels
            draw = ImageDraw.Draw(comparison)
            try:
                # Try to use a font
                font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16)
            except:
                font = ImageFont.load_default()
            
            draw.text((10, 5), "UNKNOWN", fill='red', font=font)
            draw.text((unid_resized.width + 30, 5), f"{person_name.upper()}", fill='green', font=font)
            draw.text((10, target_height + 35), f"Confidence: {confidence:.1%}", fill='blue', font=font)
            
            # Save comparison image
            temp_dir = tempfile.gettempdir()
            comparison_path = os.path.join(temp_dir, f"face_comparison_{person_name}.jpg")
            comparison.save(comparison_path, "JPEG", quality=95)
            
            return comparison_path
            
        except Exception as e:
            if self.verbose >= 1:
                print(f"⚠️  Could not create comparison image: {e}")
            return None

    def _get_confidence_description(self, confidence_pct: float) -> str:
        """Get human-readable confidence description"""
        if confidence_pct >= 80:
            return "🟢 (Very High - Almost Certain)"
        elif confidence_pct >= 70:
            return "🟡 (High - Likely Match)"
        elif confidence_pct >= 60:
            return "🟠 (Medium - Possible Match)"
        elif confidence_pct >= 50:
            return "🔴 (Low - Questionable)"
        else:
            return "⚫ (Very Low - Unlikely)"

    def _calculate_face_quality_score(self, image: np.ndarray, face_location: tuple) -> float:
        """Calculate face quality score based on multiple factors"""
        try:
            top, right, bottom, left = face_location
            face_height = bottom - top
            face_width = right - left
            
            # Basic size check - faces too small get lower scores
            min_face_size = 50
            size_score = min(1.0, (face_height * face_width) / (min_face_size * min_face_size))
            
            # Extract face region
            face_region = image[top:bottom, left:right]
            if face_region.size == 0:
                return 0.0
            
            # Convert to grayscale for analysis
            if len(face_region.shape) == 3:
                gray_face = np.mean(face_region, axis=2)
            else:
                gray_face = face_region
            
            # Calculate sharpness (Laplacian variance)
            laplacian_var = np.var(np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]]).astype(np.float32))
            if laplacian_var > 0:
                sharpness = np.var(np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]]).astype(np.float32))
            else:
                sharpness = 0.0
            sharpness_score = min(1.0, sharpness / 1000.0)  # Normalize sharpness
            
            # Calculate brightness and contrast
            mean_brightness = np.mean(gray_face)
            brightness_score = 1.0 - abs(mean_brightness - 128) / 128.0  # Prefer middle brightness
            
            contrast = np.std(gray_face)
            contrast_score = min(1.0, contrast / 64.0)  # Prefer good contrast
            
            # Calculate aspect ratio (faces should be roughly square)
            aspect_ratio = face_width / face_height if face_height > 0 else 1.0
            aspect_score = 1.0 - abs(aspect_ratio - 1.0)  # Prefer square faces
            
            # Calculate position in image (centered faces are better)
            image_height, image_width = image.shape[:2]
            center_x = (left + right) / 2
            center_y = (top + bottom) / 2
            position_x_score = 1.0 - abs(center_x - image_width / 2) / (image_width / 2)
            position_y_score = 1.0 - abs(center_y - image_height / 2) / (image_height / 2)
            position_score = (position_x_score + position_y_score) / 2.0
            
            # Weighted combination of all factors
            quality_score = (
                size_score * 0.25 +
                sharpness_score * 0.25 +
                brightness_score * 0.15 +
                contrast_score * 0.15 +
                aspect_score * 0.10 +
                position_score * 0.10
            )
            
            return max(0.0, min(1.0, quality_score))
            
        except Exception as e:
            if self.verbose >= 2:
                print(f"⚠️  Error calculating face quality: {e}")
            return 0.5  # Default medium quality on error

    def _add_person_encoding(self, person_id: int, face_id: int, encoding: np.ndarray, quality_score: float):
        """Add a face encoding to a person's encoding collection"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            cursor.execute(
                'INSERT INTO person_encodings (person_id, face_id, encoding, quality_score) VALUES (?, ?, ?, ?)',
                (person_id, face_id, encoding.tobytes(), quality_score)
            )

    def _get_person_encodings(self, person_id: int, min_quality: float = 0.3) -> List[Tuple[np.ndarray, float]]:
        """Get all high-quality encodings for a person"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            cursor.execute(
                'SELECT encoding, quality_score FROM person_encodings WHERE person_id = ? AND quality_score >= ? ORDER BY quality_score DESC',
                (person_id, min_quality)
            )
            results = cursor.fetchall()
            return [(np.frombuffer(encoding, dtype=np.float64), quality_score) for encoding, quality_score in results]

    def _update_person_encodings(self, person_id: int):
        """Update person encodings when a face is identified"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            
            # Get all faces for this person
            cursor.execute(
                'SELECT id, encoding, quality_score FROM faces WHERE person_id = ? ORDER BY quality_score DESC',
                (person_id,)
            )
            faces = cursor.fetchall()
            
            # Clear existing person encodings
            cursor.execute('DELETE FROM person_encodings WHERE person_id = ?', (person_id,))
            
            # Add all faces as person encodings
            for face_id, encoding, quality_score in faces:
                cursor.execute(
                    'INSERT INTO person_encodings (person_id, face_id, encoding, quality_score) VALUES (?, ?, ?, ?)',
                    (person_id, face_id, encoding, quality_score)
                )

    def _calculate_adaptive_tolerance(self, base_tolerance: float, face_quality: float, match_confidence: float = None) -> float:
        """Calculate adaptive tolerance based on face quality and match confidence"""
        # Start with base tolerance
        tolerance = base_tolerance
        
        # Adjust based on face quality (higher quality = stricter tolerance)
        # More conservative: range 0.9 to 1.1 instead of 0.8 to 1.2
        quality_factor = 0.9 + (face_quality * 0.2)  # Range: 0.9 to 1.1
        tolerance *= quality_factor
        
        # If we have match confidence, adjust further
        if match_confidence is not None:
            # Higher confidence matches can use stricter tolerance
            # More conservative: range 0.95 to 1.05 instead of 0.9 to 1.1
            confidence_factor = 0.95 + (match_confidence * 0.1)  # Range: 0.95 to 1.05
            tolerance *= confidence_factor
        
        # Ensure tolerance stays within reasonable bounds
        return max(0.3, min(0.8, tolerance))  # Reduced max from 0.9 to 0.8

    def _get_filtered_similar_faces(self, face_id: int, tolerance: float, include_same_photo: bool = False, face_status: dict = None) -> List[Dict]:
        """Get similar faces with consistent filtering and sorting logic used by both auto-match and identify"""
        # Find similar faces using the core function
        similar_faces_data = self.find_similar_faces(face_id, tolerance=tolerance, include_same_photo=include_same_photo)
        
        # Filter to only show unidentified faces with confidence filtering
        filtered_faces = []
        for face in similar_faces_data:
            # For auto-match: only filter by database state (keep existing behavior)
            # For identify: also filter by current session state
            is_identified_in_db = face.get('person_id') is not None
            is_identified_in_session = face_status and face.get('face_id') in face_status and face_status[face.get('face_id')] == 'identified'
            
            # If face_status is provided (identify mode), use both filters
            # If face_status is None (auto-match mode), only use database filter
            if face_status is not None:
                # Identify mode: filter out both database and session identified faces
                if not is_identified_in_db and not is_identified_in_session:
                    # Calculate confidence percentage
                    confidence_pct = (1 - face['distance']) * 100
                    
                    # Only include matches with reasonable confidence (at least 40%)
                    if confidence_pct >= 40:
                        filtered_faces.append(face)
            else:
                # Auto-match mode: only filter by database state (keep existing behavior)
                if not is_identified_in_db:
                    # Calculate confidence percentage
                    confidence_pct = (1 - face['distance']) * 100
                    
                    # Only include matches with reasonable confidence (at least 40%)
                    if confidence_pct >= 40:
                        filtered_faces.append(face)
        
        # Sort by confidence (distance) - highest confidence first
        filtered_faces.sort(key=lambda x: x['distance'])
        
        return filtered_faces

    def _filter_unique_faces(self, faces: List[Dict]) -> List[Dict]:
        """Filter faces to show only unique ones, hiding duplicates with high/medium confidence matches"""
        if not faces:
            return faces
        
        unique_faces = []
        seen_face_groups = set()  # Track face groups that have been seen
        
        for face in faces:
            face_id = face['face_id']
            confidence_pct = (1 - face['distance']) * 100
            
            # Only consider high (>=70%) or medium (>=60%) confidence matches for grouping
            if confidence_pct >= 60:
                # Find all faces that match this one with high/medium confidence
                matching_face_ids = set()
                for other_face in faces:
                    other_face_id = other_face['face_id']
                    other_confidence_pct = (1 - other_face['distance']) * 100
                    
                    # If this face matches the current face with high/medium confidence
                    if other_confidence_pct >= 60:
                        matching_face_ids.add(other_face_id)
                
                # Create a sorted tuple to represent this group of matching faces
                face_group = tuple(sorted(matching_face_ids))
                
                # Only show this face if we haven't seen this group before
                if face_group not in seen_face_groups:
                    seen_face_groups.add(face_group)
                    unique_faces.append(face)
            else:
                # For low confidence matches, always show them (they're likely different people)
                unique_faces.append(face)
        
        return unique_faces

    def _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.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 _show_people_list(self, cursor=None):
        """Show list of known people"""
        if cursor is None:
            with self.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()
        else:
            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 add_tags(self, photo_pattern: str = None, batch_size: int = 10) -> int:
        """Add custom tags to photos"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            
            if photo_pattern:
                cursor.execute(
                    'SELECT id, filename FROM photos WHERE filename LIKE ? LIMIT ?',
                    (f'%{photo_pattern}%', batch_size)
                )
            else:
                cursor.execute('SELECT id, filename FROM photos LIMIT ?', (batch_size,))
            
            photos = cursor.fetchall()
            
            if not photos:
                print("No photos found")
                return 0
            
            print(f"🏷️  Tagging {len(photos)} photos (enter comma-separated tags)")
            tagged_count = 0
            
            for photo_id, filename in photos:
                print(f"\n📸 {filename}")
                tags_input = input("🏷️  Tags: ").strip()
                
                if tags_input.lower() == 'q':
                    break
                
                if tags_input:
                    tags = [tag.strip() for tag in tags_input.split(',') if tag.strip()]
                    for tag_name in tags:
                        # First, insert or get the tag_id from tags table
                        cursor.execute(
                            'INSERT OR IGNORE INTO tags (tag_name) VALUES (?)',
                            (tag_name,)
                        )
                        cursor.execute(
                            'SELECT id FROM tags WHERE tag_name = ?',
                            (tag_name,)
                        )
                        tag_id = cursor.fetchone()[0]
                        
                        # Then, insert the linkage (ignore if already exists due to UNIQUE constraint)
                        cursor.execute(
                            'INSERT OR IGNORE INTO phototaglinkage (photo_id, tag_id) VALUES (?, ?)',
                            (photo_id, tag_id)
                        )
                    print(f"   ✅ Added {len(tags)} tags")
                    tagged_count += 1
        
        print(f"✅ Tagged {tagged_count} photos")
        return tagged_count
    
    def stats(self) -> Dict:
        """Show database statistics"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            
            stats = {}
            
            # Basic counts
            cursor.execute('SELECT COUNT(*) FROM photos')
            result = cursor.fetchone()
            stats['total_photos'] = result[0] if result else 0
            
            cursor.execute('SELECT COUNT(*) FROM photos WHERE processed = 1')
            result = cursor.fetchone()
            stats['processed_photos'] = result[0] if result else 0
            
            cursor.execute('SELECT COUNT(*) FROM faces')
            result = cursor.fetchone()
            stats['total_faces'] = result[0] if result else 0
            
            cursor.execute('SELECT COUNT(*) FROM faces WHERE person_id IS NOT NULL')
            result = cursor.fetchone()
            stats['identified_faces'] = result[0] if result else 0
            
            cursor.execute('SELECT COUNT(*) FROM people')
            result = cursor.fetchone()
            stats['total_people'] = result[0] if result else 0
            
            cursor.execute('SELECT COUNT(*) FROM tags')
            result = cursor.fetchone()
            stats['unique_tags'] = result[0] if result else 0
            
            # Top people
            cursor.execute('''
                SELECT 
                    CASE 
                        WHEN p.last_name AND p.first_name THEN p.last_name || ', ' || p.first_name
                        WHEN p.first_name THEN p.first_name
                        WHEN p.last_name THEN p.last_name
                        ELSE 'Unknown'
                    END as full_name,
                    COUNT(f.id) as face_count 
                FROM people p 
                LEFT JOIN faces f ON p.id = f.person_id 
                GROUP BY p.id 
                ORDER BY face_count DESC 
                LIMIT 15
            ''')
            stats['top_people'] = cursor.fetchall()
        
        # Display stats
        print(f"\n📊 Database Statistics")
        print("=" * 40)
        print(f"Photos: {stats['processed_photos']}/{stats['total_photos']} processed")
        print(f"Faces: {stats['identified_faces']}/{stats['total_faces']} identified")
        print(f"People: {stats['total_people']} unique")
        print(f"Tags: {stats['unique_tags']} unique")
        
        if stats['top_people']:
            print(f"\n👥 Top People:")
            for name, count in stats['top_people']:
                print(f"   {name}: {count} faces")
        
        unidentified = stats['total_faces'] - stats['identified_faces']
        if unidentified > 0:
            print(f"\n⚠️  {unidentified} faces still need identification")
        
        return stats
    
    def search_faces(self, person_name: str) -> List[str]:
        """Search for photos containing a specific person"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            
            cursor.execute('''
                SELECT DISTINCT p.filename, p.path
                FROM photos p
                JOIN faces f ON p.id = f.photo_id
                JOIN people pe ON f.person_id = pe.id
                WHERE pe.name LIKE ?
            ''', (f'%{person_name}%',))
            
            results = cursor.fetchall()
        
        if results:
            print(f"\n🔍 Found {len(results)} photos with '{person_name}':")
            for filename, path in results:
                print(f"   📸 {filename}")
        else:
            print(f"🔍 No photos found with '{person_name}'")
        
        return [path for filename, path in results]

    def find_similar_faces(self, face_id: int = None, tolerance: float = 0.6, include_same_photo: bool = False) -> List[Dict]:
        """Find similar faces across all photos with improved multi-encoding and quality scoring"""
        with self.get_db_connection() as conn:
            cursor = conn.cursor()
            
            if face_id:
                # Find faces similar to a specific face
                cursor.execute('''
                    SELECT id, photo_id, encoding, location, quality_score
                    FROM faces 
                    WHERE id = ?
                ''', (face_id,))
                target_face = cursor.fetchone()
                
                if not target_face:
                    print(f"❌ Face ID {face_id} not found")
                    return []
                
                target_encoding = self._get_cached_face_encoding(face_id, target_face[2])
                target_quality = target_face[4] if len(target_face) > 4 else 0.5
                
                # Get all other faces with quality scores
                cursor.execute('''
                    SELECT f.id, f.photo_id, f.encoding, f.location, p.filename, f.person_id, f.quality_score
                    FROM faces f
                    JOIN photos p ON f.photo_id = p.id
                    WHERE f.id != ? AND f.quality_score >= 0.2
                ''', (face_id,))
                
            else:
                # Find all unidentified faces and try to match them with identified ones
                cursor.execute('''
                    SELECT f.id, f.photo_id, f.encoding, f.location, p.filename, f.person_id, f.quality_score
                    FROM faces f
                    JOIN photos p ON f.photo_id = p.id
                    WHERE f.quality_score >= 0.2
                    ORDER BY f.quality_score DESC, f.id
                ''')
            
            all_faces = cursor.fetchall()
            matches = []
            
            if face_id:
                # Compare target face with all other faces using adaptive tolerance
                for face_data in all_faces:
                    other_id, other_photo_id, other_encoding, other_location, other_filename, other_person_id, other_quality = face_data
                    other_enc = self._get_cached_face_encoding(other_id, other_encoding)
                    
                    # Calculate adaptive tolerance based on both face qualities
                    avg_quality = (target_quality + other_quality) / 2
                    adaptive_tolerance = self._calculate_adaptive_tolerance(tolerance, avg_quality)
                    
                    distance = face_recognition.face_distance([target_encoding], other_enc)[0]
                    if distance <= adaptive_tolerance:
                        matches.append({
                            'face_id': other_id,
                            'photo_id': other_photo_id,
                            'filename': other_filename,
                            'location': other_location,
                            'distance': distance,
                            'person_id': other_person_id,
                            'quality_score': other_quality,
                            'adaptive_tolerance': adaptive_tolerance
                        })
                
                # Get target photo info
                cursor.execute('SELECT filename FROM photos WHERE id = ?', (target_face[1],))
                result = cursor.fetchone()
                target_filename = result[0] if result else "Unknown"
                
                print(f"\n🔍 Finding faces similar to face in: {target_filename}")
                print(f"📍 Target face location: {target_face[3]}")
                
            else:
                # Auto-match unidentified faces with identified ones using multi-encoding
                identified_faces = [f for f in all_faces if f[5] is not None]  # person_id is not None
                unidentified_faces = [f for f in all_faces if f[5] is None]    # person_id is None
                
                print(f"\n🔍 Auto-matching {len(unidentified_faces)} unidentified faces with {len(identified_faces)} known faces...")
                
                # Group identified faces by person (simplified for now)
                person_encodings = {}
                for id_face in identified_faces:
                    person_id = id_face[5]
                    if person_id not in person_encodings:
                        # Use single encoding per person for now (simplified)
                        id_enc = self._get_cached_face_encoding(id_face[0], id_face[2])
                        person_encodings[person_id] = [(id_enc, id_face[6])]
                
                for unid_face in unidentified_faces:
                    unid_id, unid_photo_id, unid_encoding, unid_location, unid_filename, _, unid_quality = unid_face
                    unid_enc = self._get_cached_face_encoding(unid_id, unid_encoding)
                    
                    best_match = None
                    best_distance = float('inf')
                    best_person_id = None
                    
                    # Compare with all person encodings
                    for person_id, encodings in person_encodings.items():
                        for person_enc, person_quality in encodings:
                            # Calculate adaptive tolerance based on both face qualities
                            avg_quality = (unid_quality + person_quality) / 2
                            adaptive_tolerance = self._calculate_adaptive_tolerance(tolerance, avg_quality)
                            
                            distance = face_recognition.face_distance([unid_enc], person_enc)[0]
                            
                            # Skip if same photo (unless specifically requested for twins detection)
                            # Note: Same photo check is simplified for performance
                            if not include_same_photo:
                                # For now, we'll skip this check to avoid performance issues
                                # TODO: Implement efficient same-photo checking
                                pass
                            
                            if distance <= adaptive_tolerance and distance < best_distance:
                                best_distance = distance
                                best_person_id = person_id
                                
                                # Get the best matching face info for this person
                                cursor.execute('''
                                    SELECT f.id, f.photo_id, f.location, p.filename
                                    FROM faces f
                                    JOIN photos p ON f.photo_id = p.id
                                    WHERE f.person_id = ? AND f.quality_score >= ?
                                    ORDER BY f.quality_score DESC
                                    LIMIT 1
                                ''', (person_id, 0.3))
                                
                                best_face_info = cursor.fetchone()
                                if best_face_info:
                                    best_match = {
                                        'unidentified_id': unid_id,
                                        'unidentified_photo_id': unid_photo_id,
                                        'unidentified_filename': unid_filename,
                                        'unidentified_location': unid_location,
                                        'matched_id': best_face_info[0],
                                        'matched_photo_id': best_face_info[1],
                                        'matched_filename': best_face_info[3],
                                        'matched_location': best_face_info[2],
                                        'person_id': person_id,
                                        'distance': distance,
                                        'quality_score': unid_quality,
                                        'adaptive_tolerance': adaptive_tolerance
                                    }
                    
                    if best_match:
                        matches.append(best_match)
            
            return matches

    def auto_identify_matches(self, tolerance: float = 0.6, 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.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.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._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 = {}
        
        with self.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()}
        
        print(f"✅ Pre-fetched {len(data_cache.get('person_details', {}))} person details and {len(data_cache.get('photo_paths', {}))} photo paths")
        
        identified_count = 0
        
        # Use integrated GUI for auto-matching
        import tkinter as tk
        from tkinter import ttk, messagebox
        from PIL import Image, ImageTk
        import json
        import os
        
        # 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._cleanup_face_crops()
            self.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._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
            if current_matched_index < len(matched_ids):
                matched_id = matched_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.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._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():
                # Show warning dialog with custom width
                from tkinter import messagebox
                
                # Create a custom dialog for better width control
                dialog = tk.Toplevel(root)
                dialog.title("Unsaved Changes")
                dialog.geometry("500x250")
                dialog.resizable(True, True)
                dialog.transient(root)
                dialog.grab_set()
                
                # Center the dialog
                dialog.geometry("+%d+%d" % (root.winfo_rootx() + 50, root.winfo_rooty() + 50))
                
                # Main message
                message_frame = ttk.Frame(dialog, padding="20")
                message_frame.pack(fill=tk.BOTH, expand=True)
                
                # Warning icon and text
                icon_label = ttk.Label(message_frame, text="⚠️", font=("Arial", 16))
                icon_label.pack(anchor=tk.W)
                
                main_text = ttk.Label(message_frame, 
                                    text="You have unsaved changes that will be lost if you quit.",
                                    font=("Arial", 10))
                main_text.pack(anchor=tk.W, pady=(5, 10))
                
                # Options
                options_text = ttk.Label(message_frame,
                                       text="• Yes: Save current changes and quit\n"
                                            "• No: Quit without saving\n"
                                            "• Cancel: Return to auto-match",
                                       font=("Arial", 9))
                options_text.pack(anchor=tk.W, pady=(0, 10))
                
                
                # Buttons
                button_frame = ttk.Frame(dialog)
                button_frame.pack(fill=tk.X, padx=20, pady=(0, 20))
                
                result = None
                
                def on_yes():
                    nonlocal result
                    result = True
                    dialog.destroy()
                
                def on_no():
                    nonlocal result
                    result = False
                    dialog.destroy()
                
                def on_cancel():
                    nonlocal result
                    result = None
                    dialog.destroy()
                
                yes_btn = ttk.Button(button_frame, text="Yes", command=on_yes)
                no_btn = ttk.Button(button_frame, text="No", command=on_no)
                cancel_btn = ttk.Button(button_frame, text="Cancel", command=on_cancel)
                
                yes_btn.pack(side=tk.LEFT, padx=(0, 5))
                no_btn.pack(side=tk.LEFT, padx=5)
                cancel_btn.pack(side=tk.RIGHT, padx=(5, 0))
                
                # Wait for dialog to close
                dialog.wait_window()
                
                if result is None:  # Cancel - don't quit
                    return
                elif result:  # Yes - save changes first
                    # Save current checkbox states before quitting
                    save_current_checkbox_states()
                    # Note: We don't actually save to database here, just preserve the states
                    # The user would need to click Save button for each person to persist changes
                    print("⚠️  Warning: Changes are preserved but not saved to database.")
                    print("   Click 'Save Changes' button for each person to persist changes.")
            
            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.
            """
            if current_matched_index < len(matched_ids) and match_vars:
                current_matched_id = matched_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._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
                        # Use actual image dimensions instead of assuming 300x300
                        actual_width, actual_height = pil_image.size
                        self._create_photo_icon(matched_canvas, matched_photo_path, icon_size=20,
                                              face_x=150, face_y=150, 
                                              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._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
                match_var.trace('w', lambda *args: on_checkbox_change(match_var, matched_id, match['unidentified_id']))
                
                # Configure match frame for grid layout
                match_frame.columnconfigure(0, weight=0)  # Checkbox column - fixed width
                match_frame.columnconfigure(1, weight=1)  # Text column - expandable
                match_frame.columnconfigure(2, weight=0)  # Image column - fixed width
                
                # Checkbox without text
                checkbox = ttk.Checkbutton(match_frame, variable=match_var)
                checkbox.grid(row=0, column=0, rowspan=2, sticky=(tk.W, tk.N), padx=(0, 5))
                match_checkboxes.append(checkbox)
                
                # Create labels for confidence and filename
                confidence_label = ttk.Label(match_frame, text=f"{confidence_pct:.1f}% {confidence_desc}", font=("Arial", 9, "bold"))
                confidence_label.grid(row=0, column=1, sticky=tk.W, padx=(0, 10))
                
                filename_label = ttk.Label(match_frame, text=f"📁 {match['unidentified_filename']}", font=("Arial", 8), foreground="gray")
                filename_label.grid(row=1, column=1, sticky=tk.W, padx=(0, 10))
                
                # Unidentified face image
                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=2, rowspan=2, sticky=(tk.W, tk.N), padx=(10, 0))
                    
                    unidentified_crop_path = self._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 to the unidentified face
                            self._create_photo_icon(match_canvas, unidentified_photo_path, icon_size=15,
                                                  face_x=50, face_y=50, 
                                                  face_width=100, face_height=100,
                                                  canvas_width=100, canvas_height=100)
                        except Exception as e:
                            match_canvas.create_text(50, 50, text="❌", fill="red")
                    else:
                        match_canvas.create_text(50, 50, text="🖼️", fill="gray")
            
            # 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 tag_management(self) -> int:
        """Tag management GUI - file explorer-like interface for managing photo tags"""
        import tkinter as tk
        from tkinter import ttk, messagebox
        from PIL import Image, ImageTk
        import os
        
        # Create the main window
        root = tk.Tk()
        root.title("Tag Management - Photo Explorer")
        root.resizable(True, True)
        
        # Track window state to prevent multiple destroy calls
        window_destroyed = False
        temp_crops = []
        photo_images = []  # Keep PhotoImage refs alive
        
        # Track folder expand/collapse states
        folder_states = {}  # folder_path -> is_expanded
        
        # Track pending tag changes (photo_id -> list of tag IDs)
        pending_tag_changes = {}
        # Track pending tag removals (photo_id -> list of tag IDs to remove)
        pending_tag_removals = {}
        existing_tags = []  # Cache of existing tag names from database (for UI display)
        tag_id_to_name = {}  # Cache of tag ID to name mapping
        tag_name_to_id = {}  # Cache of tag name to ID mapping
        
        # 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
            # Cleanup temp crops
            for crop in list(temp_crops):
                try:
                    if os.path.exists(crop):
                        os.remove(crop)
                except:
                    pass
            temp_crops.clear()
            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
        saved_size = self._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)
        main_frame.rowconfigure(1, weight=1)
        main_frame.rowconfigure(2, weight=0)
        
        # Title and controls frame
        header_frame = ttk.Frame(main_frame)
        header_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        header_frame.columnconfigure(1, weight=1)
        
        # Title label
        title_label = ttk.Label(header_frame, text="Photo Explorer - Tag Management", font=("Arial", 16, "bold"))
        title_label.grid(row=0, column=0, sticky=tk.W)
        
        # View mode controls
        view_frame = ttk.Frame(header_frame)
        view_frame.grid(row=0, column=1, sticky=tk.E)
        
        view_mode_var = tk.StringVar(value="list")
        ttk.Label(view_frame, text="View:").pack(side=tk.LEFT, padx=(0, 5))
        ttk.Radiobutton(view_frame, text="List", variable=view_mode_var, value="list", 
                       command=lambda: switch_view_mode("list")).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Radiobutton(view_frame, text="Icons", variable=view_mode_var, value="icons", 
                       command=lambda: switch_view_mode("icons")).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Radiobutton(view_frame, text="Compact", variable=view_mode_var, value="compact", 
                       command=lambda: switch_view_mode("compact")).pack(side=tk.LEFT)
        
        # Manage Tags button
        def open_manage_tags_dialog():
            """Open a dialog to manage tags: list, edit, add, and delete."""
            import tkinter as tk
            from tkinter import ttk, messagebox, simpledialog

            # Dialog window
            dialog = tk.Toplevel(root)
            dialog.title("Manage Tags")
            dialog.transient(root)
            dialog.grab_set()
            dialog.geometry("500x500")

            # Layout frames
            top_frame = ttk.Frame(dialog, padding="8")
            top_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
            list_frame = ttk.Frame(dialog, padding="8")
            list_frame.grid(row=1, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
            bottom_frame = ttk.Frame(dialog, padding="8")
            bottom_frame.grid(row=2, column=0, sticky=(tk.W, tk.E))

            dialog.columnconfigure(0, weight=1)
            dialog.rowconfigure(1, weight=1)

            # Add tag controls (top)
            new_tag_var = tk.StringVar()
            new_tag_entry = ttk.Entry(top_frame, textvariable=new_tag_var, width=30)
            new_tag_entry.grid(row=0, column=0, padx=(0, 8), sticky=(tk.W, tk.E))

            def add_new_tag():
                tag_name = new_tag_var.get().strip()
                if not tag_name:
                    return
                try:
                    with self.get_db_connection() as conn:
                        cursor = conn.cursor()
                        cursor.execute('INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', (tag_name,))
                        conn.commit()
                    new_tag_var.set("")
                    refresh_tag_list()
                    load_existing_tags()
                    # Refresh main view to reflect new tag options
                    switch_view_mode(view_mode_var.get())
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to add tag: {e}")

            add_btn = ttk.Button(top_frame, text="Add tag", command=add_new_tag)
            add_btn.grid(row=0, column=1, sticky=tk.W)
            top_frame.columnconfigure(0, weight=1)

            # Scrollable tag list (center)
            canvas = tk.Canvas(list_frame, highlightthickness=0)
            scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
            rows_container = ttk.Frame(canvas)
            canvas.create_window((0, 0), window=rows_container, anchor="nw")
            canvas.configure(yscrollcommand=scrollbar.set)
            canvas.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
            scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
            list_frame.columnconfigure(0, weight=1)
            list_frame.rowconfigure(0, weight=1)

            rows_container.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))

            # Selection tracking
            selected_tag_vars = {}
            current_tags = []  # list of dicts: {id, tag_name}

            def refresh_tag_list():
                # Clear rows
                for child in list(rows_container.winfo_children()):
                    child.destroy()
                selected_tag_vars.clear()
                current_tags.clear()
                # Load tags
                try:
                    with self.get_db_connection() as conn:
                        cursor = conn.cursor()
                        cursor.execute('SELECT id, tag_name FROM tags ORDER BY tag_name COLLATE NOCASE')
                        for row in cursor.fetchall():
                            current_tags.append({'id': row[0], 'tag_name': row[1]})
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to load tags: {e}")
                    return
                # Build header
                head = ttk.Frame(rows_container)
                head.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 6))
                chk_lbl = ttk.Label(head, text="Delete")
                chk_lbl.pack(side=tk.LEFT, padx=(0, 10))
                name_lbl = ttk.Label(head, text="Tag name", width=30)
                name_lbl.pack(side=tk.LEFT)
                act_lbl = ttk.Label(head, text="Edit", width=6)
                act_lbl.pack(side=tk.LEFT, padx=(10, 0))

                # Populate rows
                for idx, tag in enumerate(current_tags, start=1):
                    row = ttk.Frame(rows_container)
                    row.grid(row=idx, column=0, sticky=(tk.W, tk.E), pady=2)
                    var = tk.BooleanVar(value=False)
                    selected_tag_vars[tag['id']] = var
                    chk = ttk.Checkbutton(row, variable=var)
                    chk.pack(side=tk.LEFT, padx=(0, 10))
                    name = ttk.Label(row, text=tag['tag_name'], width=30)
                    name.pack(side=tk.LEFT)

                    def make_edit_handler(tag_id, name_label):
                        def handler():
                            new_name = simpledialog.askstring("Edit Tag", "Enter new tag name:", initialvalue=name_label.cget('text'), parent=dialog)
                            if new_name is None:
                                return
                            new_name = new_name.strip()
                            if not new_name:
                                return
                            try:
                                with self.get_db_connection() as conn:
                                    cursor = conn.cursor()
                                    # Ensure name is unique
                                    cursor.execute('UPDATE tags SET tag_name = ? WHERE id = ?', (new_name, tag_id))
                                    conn.commit()
                            except Exception as e:
                                messagebox.showerror("Error", f"Failed to rename tag: {e}")
                                return
                            # Update UI and caches
                            refresh_tag_list()
                            load_existing_tags()
                            switch_view_mode(view_mode_var.get())
                        return handler

                    edit_btn = ttk.Button(row, text="Edit", width=6, command=make_edit_handler(tag['id'], name))
                    edit_btn.pack(side=tk.LEFT, padx=(10, 0))

            refresh_tag_list()

            # Bottom buttons
            def delete_selected():
                ids_to_delete = [tid for tid, v in selected_tag_vars.items() if v.get()]
                if not ids_to_delete:
                    return
                if not messagebox.askyesno("Confirm Delete", f"Delete {len(ids_to_delete)} selected tag(s)? This will unlink them from photos."):
                    return
                try:
                    with self.get_db_connection() as conn:
                        cursor = conn.cursor()
                        # Remove linkages first to maintain integrity
                        cursor.execute(f"DELETE FROM phototaglinkage WHERE tag_id IN ({','.join('?' for _ in ids_to_delete)})", ids_to_delete)
                        # Delete tags
                        cursor.execute(f"DELETE FROM tags WHERE id IN ({','.join('?' for _ in ids_to_delete)})", ids_to_delete)
                        conn.commit()
                    
                    # Clean up pending tag changes for deleted tags
                    for photo_id in list(pending_tag_changes.keys()):
                        pending_tag_changes[photo_id] = [tid for tid in pending_tag_changes[photo_id] if tid not in ids_to_delete]
                        if not pending_tag_changes[photo_id]:
                            del pending_tag_changes[photo_id]
                    
                    # Clean up pending tag removals for deleted tags
                    for photo_id in list(pending_tag_removals.keys()):
                        pending_tag_removals[photo_id] = [tid for tid in pending_tag_removals[photo_id] if tid not in ids_to_delete]
                        if not pending_tag_removals[photo_id]:
                            del pending_tag_removals[photo_id]
                    
                    refresh_tag_list()
                    load_existing_tags()
                    load_photos()  # Refresh photo data to reflect deleted tags
                    switch_view_mode(view_mode_var.get())
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to delete tags: {e}")

            delete_btn = ttk.Button(bottom_frame, text="Delete selected tags", command=delete_selected)
            delete_btn.pack(side=tk.LEFT)
            quit_btn = ttk.Button(bottom_frame, text="Quit", command=dialog.destroy)
            quit_btn.pack(side=tk.RIGHT)

            # Keyboard focus
            new_tag_entry.focus_set()

        manage_tags_btn = ttk.Button(header_frame, text="Manage Tags", command=open_manage_tags_dialog)
        manage_tags_btn.grid(row=0, column=2, sticky=tk.E, padx=(10, 0))
        
        # Main content area
        content_frame = ttk.Frame(main_frame)
        content_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        content_frame.columnconfigure(0, weight=1)
        content_frame.rowconfigure(0, weight=1)
        
        # Style for consistent gray background
        style = ttk.Style()
        canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
        
        # Create canvas and scrollbar for content
        content_canvas = tk.Canvas(content_frame, bg=canvas_bg_color, highlightthickness=0)
        content_scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=content_canvas.yview)
        content_inner = ttk.Frame(content_canvas)
        content_canvas.create_window((0, 0), window=content_inner, anchor="nw")
        content_canvas.configure(yscrollcommand=content_scrollbar.set)
        
        content_inner.bind(
            "<Configure>",
            lambda e: content_canvas.configure(scrollregion=content_canvas.bbox("all"))
        )
        
        content_canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        content_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        
        # Bottom frame for save button
        bottom_frame = ttk.Frame(main_frame)
        bottom_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
        
        # Save tagging button (function will be defined later)
        save_button = ttk.Button(bottom_frame, text="Save Tagging")
        save_button.pack(side=tk.RIGHT, padx=10, pady=5)
        
        # Quit button with warning for pending changes
        def quit_with_warning():
            """Quit the dialog, but warn if there are pending changes"""
            # Check for pending changes
            has_pending_changes = bool(pending_tag_changes or pending_tag_removals)
            
            if has_pending_changes:
                total_additions = sum(len(tags) for tags in pending_tag_changes.values())
                total_removals = sum(len(tags) for tags in pending_tag_removals.values())
                
                changes_text = []
                if total_additions > 0:
                    changes_text.append(f"{total_additions} tag addition(s)")
                if total_removals > 0:
                    changes_text.append(f"{total_removals} tag removal(s)")
                
                changes_summary = " and ".join(changes_text)
                
                result = messagebox.askyesnocancel(
                    "Unsaved Changes", 
                    f"You have unsaved changes: {changes_summary}.\n\n"
                    "Do you want to save your changes before quitting?\n\n"
                    "Yes = Save and quit\n"
                    "No = Quit without saving\n"
                    "Cancel = Stay in dialog"
                )
                
                if result is True:  # Yes - Save and quit
                    save_tagging_changes()
                    root.destroy()
                elif result is False:  # No - Quit without saving
                    root.destroy()
                # If result is None (Cancel), do nothing - stay in dialog
            else:
                # No pending changes, just quit
                root.destroy()
        
        quit_button = ttk.Button(bottom_frame, text="Quit", command=quit_with_warning)
        quit_button.pack(side=tk.RIGHT, padx=(0, 10), pady=5)
        
        # Enable mouse scroll anywhere in the dialog
        def on_mousewheel(event):
            content_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
        
        # Column resizing variables
        resize_start_x = 0
        resize_start_widths = []
        current_visible_cols = []
        is_resizing = False
        
        def start_resize(event, col_idx):
            """Start column resizing"""
            nonlocal resize_start_x, resize_start_widths, is_resizing
            print(f"DEBUG: start_resize called for column {col_idx}, event.x_root={event.x_root}")  # Debug output
            is_resizing = True
            resize_start_x = event.x_root
            # Store current column widths
            resize_start_widths = []
            for i, col in enumerate(current_visible_cols):
                resize_start_widths.append(col['width'])
            print(f"DEBUG: Stored widths: {resize_start_widths}")  # Debug output
            # Change cursor globally
            root.configure(cursor="sb_h_double_arrow")
        
        def do_resize(event, col_idx):
            """Perform column resizing"""
            nonlocal resize_start_x, resize_start_widths, is_resizing
            print(f"DEBUG: do_resize called for column {col_idx}, is_resizing={is_resizing}")  # Debug output
            if not is_resizing or not resize_start_widths or not current_visible_cols:
                return
            
            # Calculate width change
            delta_x = event.x_root - resize_start_x
            
            # Update column widths
            if col_idx < len(current_visible_cols) and col_idx + 1 < len(current_visible_cols):
                # Resize current and next column
                new_width_left = max(50, resize_start_widths[col_idx] + delta_x)
                new_width_right = max(50, resize_start_widths[col_idx + 1] - delta_x)
                
                # Update column configuration
                current_visible_cols[col_idx]['width'] = new_width_left
                current_visible_cols[col_idx + 1]['width'] = new_width_right
                
                # Update the actual column configuration in the global config
                for i, col in enumerate(column_config['list']):
                    if col['key'] == current_visible_cols[col_idx]['key']:
                        column_config['list'][i]['width'] = new_width_left
                    elif col['key'] == current_visible_cols[col_idx + 1]['key']:
                        column_config['list'][i]['width'] = new_width_right
                
                # Force immediate visual update by reconfiguring grid weights
                try:
                    header_frame_ref = None
                    row_frames = []
                    for widget in content_inner.winfo_children():
                        # First frame is header, subsequent frames are data rows
                        if isinstance(widget, ttk.Frame):
                            if header_frame_ref is None:
                                header_frame_ref = widget
                            else:
                                row_frames.append(widget)
                    
                    # Update header columns (accounting for separator columns)
                    if header_frame_ref is not None:
                        # Update both minsize and weight to force resize
                        header_frame_ref.columnconfigure(col_idx*2, 
                                                       weight=current_visible_cols[col_idx]['weight'], 
                                                       minsize=new_width_left)
                        header_frame_ref.columnconfigure((col_idx+1)*2, 
                                                       weight=current_visible_cols[col_idx+1]['weight'], 
                                                       minsize=new_width_right)
                        print(f"DEBUG: Updated header columns {col_idx*2} and {(col_idx+1)*2} with widths {new_width_left} and {new_width_right}")
                    
                    # Update each data row frame columns (no separators, direct indices)
                    for rf in row_frames:
                        rf.columnconfigure(col_idx, 
                                         weight=current_visible_cols[col_idx]['weight'], 
                                         minsize=new_width_left)
                        rf.columnconfigure(col_idx+1, 
                                         weight=current_visible_cols[col_idx+1]['weight'], 
                                         minsize=new_width_right)
                    
                    # Force update of the display
                    root.update_idletasks()
                    
                except Exception as e:
                    print(f"DEBUG: Error during resize update: {e}")  # Debug output
                    pass  # Ignore errors during resize
        
        def stop_resize(event):
            """Stop column resizing"""
            nonlocal is_resizing
            if is_resizing:
                print(f"DEBUG: stop_resize called, was resizing={is_resizing}")  # Debug output
            is_resizing = False
            root.configure(cursor="")
        
        # Bind mouse wheel to the entire window
        root.bind_all("<MouseWheel>", on_mousewheel)
        
        # Global mouse release handler that only stops resize if we're actually resizing
        def global_mouse_release(event):
            if is_resizing:
                stop_resize(event)
        root.bind_all("<ButtonRelease-1>", global_mouse_release)
        
        # Unbind when window is destroyed
        def cleanup_mousewheel():
            try:
                root.unbind_all("<MouseWheel>")
                root.unbind_all("<ButtonRelease-1>")
            except:
                pass
        
        root.bind("<Destroy>", lambda e: cleanup_mousewheel())
        
        # Load photos from database
        photos_data = []
        
        # Column visibility state
        column_visibility = {
            'list': {'id': True, 'filename': True, 'path': True, 'processed': True, 'date_taken': True, 'faces': True, 'tags': True},
            'icons': {'thumbnail': True, 'id': True, 'filename': True, 'processed': True, 'date_taken': True, 'faces': True, 'tags': True},
            'compact': {'filename': True, 'faces': True, 'tags': True}
        }
        
        # Column order and configuration
        column_config = {
            'list': [
                {'key': 'id', 'label': 'ID', 'width': 50, 'weight': 0},
                {'key': 'filename', 'label': 'Filename', 'width': 150, 'weight': 1},
                {'key': 'path', 'label': 'Path', 'width': 200, 'weight': 2},
                {'key': 'processed', 'label': 'Processed', 'width': 80, 'weight': 0},
                {'key': 'date_taken', 'label': 'Date Taken', 'width': 120, 'weight': 0},
                {'key': 'faces', 'label': 'Faces', 'width': 60, 'weight': 0},
                {'key': 'tags', 'label': 'Tags', 'width': 180, 'weight': 1}
            ],
            'icons': [
                {'key': 'thumbnail', 'label': 'Thumbnail', 'width': 160, 'weight': 0},
                {'key': 'id', 'label': 'ID', 'width': 80, 'weight': 0},
                {'key': 'filename', 'label': 'Filename', 'width': 200, 'weight': 1},
                {'key': 'processed', 'label': 'Processed', 'width': 80, 'weight': 0},
                {'key': 'date_taken', 'label': 'Date Taken', 'width': 120, 'weight': 0},
                {'key': 'faces', 'label': 'Faces', 'width': 60, 'weight': 0},
                {'key': 'tags', 'label': 'Tags', 'width': 180, 'weight': 1}
            ],
            'compact': [
                {'key': 'filename', 'label': 'Filename', 'width': 200, 'weight': 1},
                {'key': 'faces', 'label': 'Faces', 'width': 60, 'weight': 0},
                {'key': 'tags', 'label': 'Tags', 'width': 180, 'weight': 1}
            ]
        }
        
        def load_photos():
            nonlocal photos_data
            with self.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    SELECT p.id, p.filename, p.path, p.processed, p.date_taken, p.date_added,
                           COUNT(f.id) as face_count,
                           GROUP_CONCAT(DISTINCT t.tag_name) as tags
                    FROM photos p
                    LEFT JOIN faces f ON f.photo_id = p.id
                    LEFT JOIN phototaglinkage ptl ON ptl.photo_id = p.id
                    LEFT JOIN tags t ON t.id = ptl.tag_id
                    GROUP BY p.id, p.filename, p.path, p.processed, p.date_taken, p.date_added
                    ORDER BY p.date_taken DESC, p.filename
                ''')
                photos_data = []
                for row in cursor.fetchall():
                    photos_data.append({
                        'id': row[0],
                        'filename': row[1],
                        'path': row[2],
                        'processed': row[3],
                        'date_taken': row[4],
                        'date_added': row[5],
                        'face_count': row[6] or 0,
                        'tags': row[7] or ""
                    })
        
        def prepare_folder_grouped_data():
            """Prepare photo data grouped by folders"""
            import os
            from collections import defaultdict
            
            # Group photos by folder
            folder_groups = defaultdict(list)
            for photo in photos_data:
                folder_path = os.path.dirname(photo['path'])
                folder_name = os.path.basename(folder_path) if folder_path else "Root"
                folder_groups[folder_path].append(photo)
            
            # Sort folders by path and photos within each folder by date_taken
            sorted_folders = []
            for folder_path in sorted(folder_groups.keys()):
                folder_name = os.path.basename(folder_path) if folder_path else "Root"
                photos_in_folder = sorted(folder_groups[folder_path], 
                                        key=lambda x: x['date_taken'] or '', reverse=True)
                
                # Initialize folder state if not exists (default to expanded)
                if folder_path not in folder_states:
                    folder_states[folder_path] = True
                
                sorted_folders.append({
                    'folder_path': folder_path,
                    'folder_name': folder_name,
                    'photos': photos_in_folder,
                    'photo_count': len(photos_in_folder)
                })
            
            return sorted_folders
        
        def create_folder_header(parent, folder_info, current_row, col_count, view_mode):
            """Create a collapsible folder header with toggle button"""
            # Create folder header frame
            folder_header_frame = ttk.Frame(parent)
            folder_header_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(10, 5))
            folder_header_frame.configure(relief='raised', borderwidth=1)
            
            # Create toggle button
            is_expanded = folder_states.get(folder_info['folder_path'], True)
            toggle_text = "▼" if is_expanded else "▶"
            toggle_button = tk.Button(folder_header_frame, text=toggle_text, width=2, height=1,
                                    command=lambda: toggle_folder(folder_info['folder_path'], view_mode),
                                    font=("Arial", 8), relief='flat', bd=1)
            toggle_button.pack(side=tk.LEFT, padx=(5, 2), pady=5)
            
            # Create folder label
            folder_label = ttk.Label(folder_header_frame, 
                                   text=f"📁 {folder_info['folder_name']} ({folder_info['photo_count']} photos)",
                                   font=("Arial", 11, "bold"))
            folder_label.pack(side=tk.LEFT, padx=(0, 10), pady=5)
            
            return folder_header_frame
        
        def toggle_folder(folder_path, view_mode):
            """Toggle folder expand/collapse state and refresh view"""
            folder_states[folder_path] = not folder_states.get(folder_path, True)
            switch_view_mode(view_mode)
        
        def load_existing_tags():
            """Load existing tags from database"""
            nonlocal existing_tags, tag_id_to_name, tag_name_to_id
            with self.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('SELECT id, tag_name FROM tags ORDER BY tag_name')
                existing_tags = []
                tag_id_to_name = {}
                tag_name_to_id = {}
                for row in cursor.fetchall():
                    tag_id, tag_name = row
                    existing_tags.append(tag_name)
                    tag_id_to_name[tag_id] = tag_name
                    tag_name_to_id[tag_name] = tag_id
        
        def create_tagging_widget(parent, photo_id, current_tags=""):
            """Create a tagging widget with dropdown and text input"""
            import tkinter as tk
            from tkinter import ttk
            
            # Create frame for tagging widget
            tagging_frame = ttk.Frame(parent)
            
            # Create combobox for tag selection/input
            tag_var = tk.StringVar()
            tag_combo = ttk.Combobox(tagging_frame, textvariable=tag_var, width=12)
            tag_combo['values'] = existing_tags
            tag_combo.pack(side=tk.LEFT, padx=2, pady=2)
            
            # Create label to show current pending tags
            pending_tags_var = tk.StringVar()
            pending_tags_label = ttk.Label(tagging_frame, textvariable=pending_tags_var, 
                                         font=("Arial", 8), foreground="blue", width=20)
            pending_tags_label.pack(side=tk.LEFT, padx=2, pady=2)
            
            # Initialize pending tags display
            if photo_id in pending_tag_changes:
                # Convert tag IDs to names for display
                pending_tag_names = [tag_id_to_name.get(tag_id, f"Unknown {tag_id}") for tag_id in pending_tag_changes[photo_id]]
                pending_tags_var.set(", ".join(pending_tag_names))
            else:
                pending_tags_var.set(current_tags or "")
            
            # Add button to add tag
            def add_tag():
                tag_name = tag_var.get().strip()
                if tag_name:
                    # Get or create tag ID
                    if tag_name in tag_name_to_id:
                        tag_id = tag_name_to_id[tag_name]
                    else:
                        # Create new tag in database
                        with self.get_db_connection() as conn:
                            cursor = conn.cursor()
                            cursor.execute('INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', (tag_name,))
                            cursor.execute('SELECT id FROM tags WHERE tag_name = ?', (tag_name,))
                            tag_id = cursor.fetchone()[0]
                            # Update mappings
                            tag_name_to_id[tag_name] = tag_id
                            tag_id_to_name[tag_id] = tag_name
                            if tag_name not in existing_tags:
                                existing_tags.append(tag_name)
                                existing_tags.sort()
                    
                    # Check if tag already exists (compare tag IDs) before adding to pending changes
                    existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id)
                    pending_tag_ids = pending_tag_changes.get(photo_id, [])
                    all_existing_tag_ids = existing_tag_ids + pending_tag_ids
                    
                    if tag_id not in all_existing_tag_ids:
                        # Only add to pending changes if tag is actually new
                        if photo_id not in pending_tag_changes:
                            pending_tag_changes[photo_id] = []
                        pending_tag_changes[photo_id].append(tag_id)
                        # Update display
                        pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]]
                        pending_tags_var.set(", ".join(pending_tag_names))
                        tag_var.set("")  # Clear the input field
            
            add_button = tk.Button(tagging_frame, text="+", width=2, height=1, command=add_tag)
            add_button.pack(side=tk.LEFT, padx=2, pady=2)
            
            # Remove button to remove last tag
            def remove_tag():
                if photo_id in pending_tag_changes and pending_tag_changes[photo_id]:
                    pending_tag_changes[photo_id].pop()
                    if pending_tag_changes[photo_id]:
                        # Convert tag IDs to names for display
                        pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]]
                        pending_tags_var.set(", ".join(pending_tag_names))
                    else:
                        pending_tags_var.set("")
                        del pending_tag_changes[photo_id]
            
            remove_button = tk.Button(tagging_frame, text="-", width=2, height=1, command=remove_tag)
            remove_button.pack(side=tk.LEFT, padx=2, pady=2)
            
            return tagging_frame
        
        def save_tagging_changes():
            """Save all pending tag changes to database"""
            if not pending_tag_changes and not pending_tag_removals:
                messagebox.showinfo("Info", "No tag changes to save.")
                return
            
            try:
                with self.get_db_connection() as conn:
                    cursor = conn.cursor()
                    
                    # Handle tag additions
                    for photo_id, tag_ids in pending_tag_changes.items():
                        for tag_id in tag_ids:
                            # Insert linkage (ignore if already exists)
                            cursor.execute(
                                'INSERT OR IGNORE INTO phototaglinkage (photo_id, tag_id) VALUES (?, ?)',
                                (photo_id, tag_id)
                            )
                    
                    # Handle tag removals
                    for photo_id, tag_ids in pending_tag_removals.items():
                        for tag_id in tag_ids:
                            # Remove linkage
                            cursor.execute(
                                'DELETE FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?',
                                (photo_id, tag_id)
                            )
                    
                    conn.commit()
                    
                # Store counts before clearing
                saved_additions = len(pending_tag_changes)
                saved_removals = len(pending_tag_removals)
                    
                # Clear pending changes and reload data
                pending_tag_changes.clear()
                pending_tag_removals.clear()
                load_existing_tags()
                load_photos()
                switch_view_mode(view_mode_var.get())
                update_save_button_text()
                
                message = f"Saved {saved_additions} tag additions"
                if saved_removals > 0:
                    message += f" and {saved_removals} tag removals"
                message += "."
                messagebox.showinfo("Success", message)
                
            except Exception as e:
                messagebox.showerror("Error", f"Failed to save tags: {str(e)}")
        
        def update_save_button_text():
            """Update save button text to show pending changes count"""
            total_additions = sum(len(tags) for tags in pending_tag_changes.values())
            total_removals = sum(len(tags) for tags in pending_tag_removals.values())
            total_changes = total_additions + total_removals
            
            if total_changes > 0:
                save_button.configure(text=f"Save Tagging ({total_changes} pending)")
            else:
                save_button.configure(text="Save Tagging")
        
        # Configure the save button command now that the function is defined
        save_button.configure(command=save_tagging_changes)
        
        def clear_content():
            for widget in content_inner.winfo_children():
                widget.destroy()
            # Cleanup temp crops
            for crop in list(temp_crops):
                try:
                    if os.path.exists(crop):
                        os.remove(crop)
                except:
                    pass
            temp_crops.clear()
            photo_images.clear()
        
        def show_column_context_menu(event, view_mode):
            """Show context menu for column visibility"""
            # Create a custom popup window instead of a menu
            popup = tk.Toplevel(root)
            popup.wm_overrideredirect(True)
            popup.wm_geometry(f"+{event.x_root}+{event.y_root}")
            popup.configure(bg='white', relief='flat', bd=0)
            
            # Define columns that cannot be hidden
            protected_columns = {
                'icons': ['thumbnail'],
                'compact': ['filename'],
                'list': ['filename']
            }
            
            # Create frame for menu items
            menu_frame = tk.Frame(popup, bg='white')
            menu_frame.pack(padx=2, pady=2)
            
            # Variables to track checkbox states
            checkbox_vars = {}
            
            for col in column_config[view_mode]:
                key = col['key']
                label = col['label']
                is_visible = column_visibility[view_mode][key]
                is_protected = key in protected_columns.get(view_mode, [])
                
                # Create frame for this menu item
                item_frame = tk.Frame(menu_frame, bg='white', relief='flat', bd=0)
                item_frame.pack(fill=tk.X, pady=1)
                
                # Create checkbox variable
                var = tk.BooleanVar(value=is_visible)
                checkbox_vars[key] = var
                
                def make_toggle_command(col_key, var_ref):
                    def toggle_column():
                        if col_key in protected_columns.get(view_mode, []):
                            return
                        # The checkbox has already toggled its state automatically
                        # Just sync it with our column visibility
                        column_visibility[view_mode][col_key] = var_ref.get()
                        # Refresh the view
                        switch_view_mode(view_mode)
                    return toggle_column
                
                if is_protected:
                    # Protected columns - disabled checkbox
                    cb = tk.Checkbutton(item_frame, text=label, variable=var, 
                                      state='disabled', bg='white', fg='gray',
                                      font=("Arial", 9), relief='flat', bd=0,
                                      highlightthickness=0)
                    cb.pack(side=tk.LEFT, padx=5, pady=2)
                    tk.Label(item_frame, text="(always visible)", bg='white', fg='gray', 
                            font=("Arial", 8)).pack(side=tk.LEFT, padx=(0, 5))
                else:
                    # Regular columns - clickable checkbox
                    cb = tk.Checkbutton(item_frame, text=label, variable=var,
                                      command=make_toggle_command(key, var),
                                      bg='white', font=("Arial", 9), relief='flat', bd=0,
                                      highlightthickness=0)
                    cb.pack(side=tk.LEFT, padx=5, pady=2)
            
            # Function to close popup
            def close_popup():
                try:
                    popup.destroy()
                except:
                    pass
            
            # Bind events to close popup
            def close_on_click_outside(event):
                # Close popup when clicking anywhere in the main window
                # Check if the click is not on the popup itself
                if event.widget != popup:
                    try:
                        # Check if popup still exists
                        popup.winfo_exists()
                        # If we get here, popup exists, so close it
                        close_popup()
                    except tk.TclError:
                        # Popup was already destroyed, do nothing
                        pass
            
            root.bind("<Button-1>", close_on_click_outside)
            root.bind("<Button-3>", close_on_click_outside)
            
            # Also bind to the main content area
            content_canvas.bind("<Button-1>", close_on_click_outside)
            content_canvas.bind("<Button-3>", close_on_click_outside)
            
            # Focus the popup
            popup.focus_set()
        
        # Shared tag linking functions for all view modes
        def create_add_tag_handler(photo_id, label_widget, photo_tags, available_tags):
            """Create a handler function for adding tags to a photo"""
            def handler():
                # Create popup window for tag management
                popup = tk.Toplevel(root)
                popup.title("Manage Photo Tags")
                popup.transient(root)
                popup.grab_set()
                popup.geometry("500x400")
                popup.resizable(True, True)
                
                # Layout frames
                top_frame = ttk.Frame(popup, padding="8")
                top_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
                list_frame = ttk.Frame(popup, padding="8")
                list_frame.grid(row=1, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
                bottom_frame = ttk.Frame(popup, padding="8")
                bottom_frame.grid(row=2, column=0, sticky=(tk.W, tk.E))
                
                popup.columnconfigure(0, weight=1)
                popup.rowconfigure(1, weight=1)
                
                # Top frame - dropdown to select tag to add
                ttk.Label(top_frame, text="Add tag:").grid(row=0, column=0, padx=(0, 8), sticky=tk.W)
                tag_var = tk.StringVar()
                combo = ttk.Combobox(top_frame, textvariable=tag_var, values=available_tags, width=30, state='readonly')
                combo.grid(row=0, column=1, padx=(0, 8), sticky=(tk.W, tk.E))
                combo.focus_set()
                
                def add_selected_tag():
                    tag_name = tag_var.get().strip()
                    if not tag_name:
                        return
                    
                    # Get or create tag ID
                    if tag_name in tag_name_to_id:
                        tag_id = tag_name_to_id[tag_name]
                    else:
                        # Create new tag in database
                        with self.get_db_connection() as conn:
                            cursor = conn.cursor()
                            cursor.execute('INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', (tag_name,))
                            cursor.execute('SELECT id FROM tags WHERE tag_name = ?', (tag_name,))
                            tag_id = cursor.fetchone()[0]
                            # Update mappings
                            tag_name_to_id[tag_name] = tag_id
                            tag_id_to_name[tag_id] = tag_name
                            if tag_name not in existing_tags:
                                existing_tags.append(tag_name)
                                existing_tags.sort()
                    
                    # Check if tag already exists (compare tag IDs) before adding to pending changes
                    existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id)
                    pending_tag_ids = pending_tag_changes.get(photo_id, [])
                    all_existing_tag_ids = existing_tag_ids + pending_tag_ids
                    
                    if tag_id not in all_existing_tag_ids:
                        # Only add to pending changes if tag is actually new
                        if photo_id not in pending_tag_changes:
                            pending_tag_changes[photo_id] = []
                        pending_tag_changes[photo_id].append(tag_id)
                        refresh_tag_list()
                        update_save_button_text()
                    
                    tag_var.set("")  # Clear the dropdown
                
                add_btn = ttk.Button(top_frame, text="Add", command=add_selected_tag)
                add_btn.grid(row=0, column=2, padx=(0, 8))
                
                # List frame - show all linked tags (existing + pending) with checkboxes
                ttk.Label(list_frame, text="Linked tags (check to remove):", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(0, 5))
                
                # Create scrollable frame for tags
                canvas = tk.Canvas(list_frame, height=200)
                scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
                scrollable_frame = ttk.Frame(canvas)
                
                scrollable_frame.bind(
                    "<Configure>",
                    lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
                )
                
                canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
                canvas.configure(yscrollcommand=scrollbar.set)
                
                canvas.pack(side="left", fill="both", expand=True)
                scrollbar.pack(side="right", fill="y")
                
                # Variables to track selected tags for removal
                selected_tag_vars = {}
                
                def refresh_tag_list():
                    # Clear existing widgets
                    for widget in scrollable_frame.winfo_children():
                        widget.destroy()
                    selected_tag_vars.clear()
                    
                    # Get existing tags for this photo
                    existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id)
                    
                    # Get pending tags for this photo
                    pending_tag_ids = pending_tag_changes.get(photo_id, [])
                    
                    # Get pending removals for this photo
                    pending_removal_ids = pending_tag_removals.get(photo_id, [])
                    
                    # Combine and deduplicate tag IDs, but exclude tags marked for removal
                    all_tag_ids = existing_tag_ids + pending_tag_ids
                    unique_tag_ids = list(set(all_tag_ids))  # Remove duplicates
                    # Remove tags that are marked for removal
                    unique_tag_ids = [tid for tid in unique_tag_ids if tid not in pending_removal_ids]
                    
                    # Convert to names for display
                    unique_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in unique_tag_ids]
                    
                    if not unique_tag_names:
                        ttk.Label(scrollable_frame, text="No tags linked to this photo", 
                                foreground="gray").pack(anchor=tk.W, pady=5)
                        return
                    
                    # Create checkboxes for each tag
                    for i, tag_id in enumerate(unique_tag_ids):
                        tag_name = tag_id_to_name.get(tag_id, f"Unknown {tag_id}")
                        var = tk.BooleanVar()
                        selected_tag_vars[tag_name] = var
                        
                        # Determine if this is a pending tag
                        is_pending = tag_id in pending_tag_ids
                        status_text = " (pending)" if is_pending else " (saved)"
                        status_color = "blue" if is_pending else "black"
                        
                        frame = ttk.Frame(scrollable_frame)
                        frame.pack(fill=tk.X, pady=1)
                        
                        checkbox = ttk.Checkbutton(frame, variable=var)
                        checkbox.pack(side=tk.LEFT, padx=(0, 5))
                        
                        label = ttk.Label(frame, text=tag_name + status_text, foreground=status_color)
                        label.pack(side=tk.LEFT)
                
                def remove_selected_tags():
                    # Get tag IDs to remove (convert names to IDs)
                    tag_ids_to_remove = []
                    for tag_name, var in selected_tag_vars.items():
                        if var.get() and tag_name in tag_name_to_id:
                            tag_ids_to_remove.append(tag_name_to_id[tag_name])
                    
                    if not tag_ids_to_remove:
                        return
                    
                    # Remove from pending changes (using IDs)
                    if photo_id in pending_tag_changes:
                        pending_tag_changes[photo_id] = [
                            tid for tid in pending_tag_changes[photo_id] 
                            if tid not in tag_ids_to_remove
                        ]
                        if not pending_tag_changes[photo_id]:
                            del pending_tag_changes[photo_id]
                    
                    # Track removals for saved tags (using IDs)
                    existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id)
                    for tag_id in tag_ids_to_remove:
                        if tag_id in existing_tag_ids:
                            # This is a saved tag, add to pending removals
                            if photo_id not in pending_tag_removals:
                                pending_tag_removals[photo_id] = []
                            if tag_id not in pending_tag_removals[photo_id]:
                                pending_tag_removals[photo_id].append(tag_id)
                    
                    refresh_tag_list()
                    update_save_button_text()
                
                # Bottom frame - buttons
                remove_btn = ttk.Button(bottom_frame, text="Remove selected tags", command=remove_selected_tags)
                remove_btn.pack(side=tk.LEFT, padx=(0, 8))
                
                close_btn = ttk.Button(bottom_frame, text="Close", command=popup.destroy)
                close_btn.pack(side=tk.RIGHT)
                
                # Initial load
                refresh_tag_list()
                
                # Update main display when dialog closes
                def on_close():
                    # Update the main display
                    existing_tags_list = self._parse_tags_string(photo_tags)
                    
                    # Get pending tag IDs and convert to names
                    pending_tag_names = []
                    if photo_id in pending_tag_changes:
                        pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]]
                    
                    # Get pending removal IDs and convert to names
                    pending_removal_names = []
                    if photo_id in pending_tag_removals:
                        pending_removal_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_removals[photo_id]]
                    
                    all_tags = existing_tags_list + pending_tag_names
                    unique_tags = self._deduplicate_tags(all_tags)
                    # Remove tags marked for removal
                    unique_tags = [tag for tag in unique_tags if tag not in pending_removal_names]
                    current_tags = ", ".join(unique_tags) if unique_tags else "None"
                    label_widget.configure(text=current_tags)
                    popup.destroy()
                
                popup.protocol("WM_DELETE_WINDOW", on_close)
                
            return handler
        
        
        def create_tag_buttons_frame(parent, photo_id, photo_tags, existing_tags, use_grid=False, row=0, col=0):
            """Create a frame with tag display and add button that can be used in any view mode"""
            tags_frame = ttk.Frame(parent)
            
            # Display current tags
            existing_tags_list = self._parse_tags_string(photo_tags)
            
            # Get pending tag names
            pending_tag_names = []
            if photo_id in pending_tag_changes:
                pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]]
            
            # Get pending removal names
            pending_removal_names = []
            if photo_id in pending_tag_removals:
                pending_removal_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_removals[photo_id]]
            
            all_tags = existing_tags_list + pending_tag_names
            unique_tags = self._deduplicate_tags(all_tags)
            # Remove tags marked for removal
            unique_tags = [tag for tag in unique_tags if tag not in pending_removal_names]
            current_display = ", ".join(unique_tags) if unique_tags else "None"
            
            tags_text = ttk.Label(tags_frame, text=current_display)
            tags_text.pack(side=tk.LEFT)
            
            # Add button with linkage icon
            add_btn = tk.Button(tags_frame, text="🔗", width=2, 
                              command=create_add_tag_handler(photo_id, tags_text, photo_tags, existing_tags))
            add_btn.pack(side=tk.LEFT, padx=(6, 0))
            
            # Pack or grid the frame based on the view mode
            if use_grid:
                tags_frame.grid(row=row, column=col, padx=5, sticky=tk.W)
            else:
                tags_frame.pack(side=tk.LEFT, padx=5)
            
            return tags_frame
        
        def show_list_view():
            clear_content()
            
            # Get visible columns and store globally for resize functions
            nonlocal current_visible_cols
            current_visible_cols = [col.copy() for col in column_config['list'] if column_visibility['list'][col['key']]]
            col_count = len(current_visible_cols)
            
            if col_count == 0:
                ttk.Label(content_inner, text="No columns visible. Right-click to show columns.", 
                         font=("Arial", 12), foreground="gray").grid(row=0, column=0, pady=20)
                return
            
            # Configure column weights for visible columns
            for i, col in enumerate(current_visible_cols):
                content_inner.columnconfigure(i, weight=col['weight'], minsize=col['width'])
            
            # Create header row
            header_frame = ttk.Frame(content_inner)
            header_frame.grid(row=0, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
            
            # Configure header frame columns (accounting for separators)
            for i, col in enumerate(current_visible_cols):
                header_frame.columnconfigure(i*2, weight=col['weight'], minsize=col['width'])
                if i < len(current_visible_cols) - 1:
                    header_frame.columnconfigure(i*2+1, weight=0, minsize=1)  # Separator column
            
            # Create header labels with right-click context menu and resizable separators
            for i, col in enumerate(current_visible_cols):
                header_label = ttk.Label(header_frame, text=col['label'], font=("Arial", 10, "bold"))
                header_label.grid(row=0, column=i*2, padx=5, sticky=tk.W)
                # Bind right-click to each label as well
                header_label.bind("<Button-3>", lambda e, mode='list': show_column_context_menu(e, mode))
                
                # Add resizable vertical separator after each column (except the last one)
                if i < len(current_visible_cols) - 1:
                    # Create a more visible separator frame with inner dark line
                    separator_frame = tk.Frame(header_frame, width=10, bg='red', cursor="sb_h_double_arrow")  # Bright red for debugging
                    separator_frame.grid(row=0, column=i*2+1, sticky='ns', padx=0)
                    separator_frame.grid_propagate(False)  # Maintain fixed width
                    # Inner dark line for better contrast
                    inner_line = tk.Frame(separator_frame, bg='darkred', width=2)  # Dark red for debugging
                    inner_line.pack(fill=tk.Y, expand=True)
                    
                    # Make separator resizable
                    separator_frame.bind("<Button-1>", lambda e, col_idx=i: start_resize(e, col_idx))
                    separator_frame.bind("<B1-Motion>", lambda e, col_idx=i: do_resize(e, col_idx))
                    separator_frame.bind("<ButtonRelease-1>", stop_resize)
                    separator_frame.bind("<Enter>", lambda e, f=separator_frame, l=inner_line: (f.configure(bg='orange'), l.configure(bg='darkorange')))  # Orange for debugging
                    separator_frame.bind("<Leave>", lambda e, f=separator_frame, l=inner_line: (f.configure(bg='red'), l.configure(bg='darkred')))  # Red for debugging
                    
                    # Also bind to the inner line for better hit detection
                    inner_line.bind("<Button-1>", lambda e, col_idx=i: start_resize(e, col_idx))
                    inner_line.bind("<B1-Motion>", lambda e, col_idx=i: do_resize(e, col_idx))
                    inner_line.bind("<ButtonRelease-1>", stop_resize)
            
            # Bind right-click to the entire header frame
            header_frame.bind("<Button-3>", lambda e, mode='list': show_column_context_menu(e, mode))
            
            # Add separator
            ttk.Separator(content_inner, orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
            
            # Get folder-grouped data
            folder_data = prepare_folder_grouped_data()
            
            # Add folder sections and photo rows
            current_row = 2
            for folder_info in folder_data:
                # Add collapsible folder header
                create_folder_header(content_inner, folder_info, current_row, col_count, 'list')
                current_row += 1
                
                # Add photos in this folder only if expanded
                if folder_states.get(folder_info['folder_path'], True):
                    for photo in folder_info['photos']:
                        row_frame = ttk.Frame(content_inner)
                        row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=1)
                        
                        # Configure row frame columns (no separators in data rows)
                        for i, col in enumerate(current_visible_cols):
                            row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
                        
                        for i, col in enumerate(current_visible_cols):
                            key = col['key']
                            if key == 'id':
                                text = str(photo['id'])
                            elif key == 'filename':
                                text = photo['filename']
                            elif key == 'path':
                                text = photo['path']
                            elif key == 'processed':
                                text = "Yes" if photo['processed'] else "No"
                            elif key == 'date_taken':
                                text = photo['date_taken'] or "Unknown"
                            elif key == 'faces':
                                text = str(photo['face_count'])
                            elif key == 'tags':
                                # Use shared tag buttons frame for list view
                                create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], existing_tags, use_grid=True, row=0, col=i)
                                continue
                            
                            ttk.Label(row_frame, text=text).grid(row=0, column=i, padx=5, sticky=tk.W)
                        
                        current_row += 1
        
        def show_icon_view():
            clear_content()
            
            # Get visible columns
            visible_cols = [col for col in column_config['icons'] if column_visibility['icons'][col['key']]]
            col_count = len(visible_cols)
            
            if col_count == 0:
                ttk.Label(content_inner, text="No columns visible. Right-click to show columns.", 
                         font=("Arial", 12), foreground="gray").grid(row=0, column=0, pady=20)
                return
            
            # Configure column weights for visible columns
            for i, col in enumerate(visible_cols):
                content_inner.columnconfigure(i, weight=col['weight'], minsize=col['width'])
            
            # Create header row
            header_frame = ttk.Frame(content_inner)
            header_frame.grid(row=0, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
            
            for i, col in enumerate(visible_cols):
                header_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
            
            # Create header labels with right-click context menu
            for i, col in enumerate(visible_cols):
                header_label = ttk.Label(header_frame, text=col['label'], font=("Arial", 10, "bold"))
                header_label.grid(row=0, column=i, padx=5, sticky=tk.W)
                # Bind right-click to each label as well
                header_label.bind("<Button-3>", lambda e, mode='icons': show_column_context_menu(e, mode))
            
            # Bind right-click to the entire header frame
            header_frame.bind("<Button-3>", lambda e, mode='icons': show_column_context_menu(e, mode))
            
            # Add separator
            ttk.Separator(content_inner, orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
            
            # Get folder-grouped data
            folder_data = prepare_folder_grouped_data()
            
            # Show photos grouped by folders
            current_row = 2
            for folder_info in folder_data:
                # Add collapsible folder header
                create_folder_header(content_inner, folder_info, current_row, col_count, 'icons')
                current_row += 1
                
                # Add photos in this folder only if expanded
                if folder_states.get(folder_info['folder_path'], True):
                    for photo in folder_info['photos']:
                        row_frame = ttk.Frame(content_inner)
                        row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=2)
                        
                        for i, col in enumerate(visible_cols):
                            row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
                        
                        col_idx = 0
                        for col in visible_cols:
                            key = col['key']
                            
                            if key == 'thumbnail':
                                # Thumbnail column
                                thumbnail_frame = ttk.Frame(row_frame)
                                thumbnail_frame.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                                
                                try:
                                    if os.path.exists(photo['path']):
                                        img = Image.open(photo['path'])
                                        img.thumbnail((150, 150), Image.Resampling.LANCZOS)
                                        photo_img = ImageTk.PhotoImage(img)
                                        photo_images.append(photo_img)
                                        
                                        # Create canvas for image
                                        canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg=canvas_bg_color, highlightthickness=0)
                                        canvas.pack()
                                        canvas.create_image(75, 75, image=photo_img)
                                    else:
                                        # Placeholder for missing image
                                        canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg=canvas_bg_color, highlightthickness=0)
                                        canvas.pack()
                                        canvas.create_text(75, 75, text="🖼️", fill="gray", font=("Arial", 24))
                                except Exception:
                                    # Error loading image
                                    canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg=canvas_bg_color, highlightthickness=0)
                                    canvas.pack()
                                    canvas.create_text(75, 75, text="❌", fill="red", font=("Arial", 24))
                            else:
                                # Data columns
                                if key == 'id':
                                    text = str(photo['id'])
                                elif key == 'filename':
                                    text = photo['filename']
                                elif key == 'processed':
                                    text = "Yes" if photo['processed'] else "No"
                                elif key == 'date_taken':
                                    text = photo['date_taken'] or "Unknown"
                                elif key == 'faces':
                                    text = str(photo['face_count'])
                                elif key == 'tags':
                                    # Use shared tag buttons frame for icon view
                                    create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], existing_tags, use_grid=True, row=0, col=col_idx)
                                    col_idx += 1
                                    continue
                                
                                ttk.Label(row_frame, text=text).grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                            
                            col_idx += 1
                        
                        current_row += 1
        
        def show_compact_view():
            clear_content()
            
            # Get visible columns
            visible_cols = [col for col in column_config['compact'] if column_visibility['compact'][col['key']]]
            col_count = len(visible_cols)
            
            if col_count == 0:
                ttk.Label(content_inner, text="No columns visible. Right-click to show columns.", 
                         font=("Arial", 12), foreground="gray").grid(row=0, column=0, pady=20)
                return
            
            # Configure column weights for visible columns
            for i, col in enumerate(visible_cols):
                content_inner.columnconfigure(i, weight=col['weight'], minsize=col['width'])
            
            # Create header
            header_frame = ttk.Frame(content_inner)
            header_frame.grid(row=0, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
            
            for i, col in enumerate(visible_cols):
                header_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
            
            # Create header labels with right-click context menu
            for i, col in enumerate(visible_cols):
                header_label = ttk.Label(header_frame, text=col['label'], font=("Arial", 10, "bold"))
                header_label.grid(row=0, column=i, padx=5, sticky=tk.W)
                # Bind right-click to each label as well
                header_label.bind("<Button-3>", lambda e, mode='compact': show_column_context_menu(e, mode))
            
            # Bind right-click to the entire header frame
            header_frame.bind("<Button-3>", lambda e, mode='compact': show_column_context_menu(e, mode))
            
            # Add separator
            ttk.Separator(content_inner, orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
            
            # Get folder-grouped data
            folder_data = prepare_folder_grouped_data()
            
            # Add folder sections and photo rows
            current_row = 2
            for folder_info in folder_data:
                # Add collapsible folder header
                create_folder_header(content_inner, folder_info, current_row, col_count, 'compact')
                current_row += 1
                
                # Add photos in this folder only if expanded
                if folder_states.get(folder_info['folder_path'], True):
                    for photo in folder_info['photos']:
                        row_frame = ttk.Frame(content_inner)
                        row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=1)
                        
                        for i, col in enumerate(visible_cols):
                            row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
                        
                        col_idx = 0
                        for col in visible_cols:
                            key = col['key']
                            if key == 'filename':
                                text = photo['filename']
                            elif key == 'faces':
                                text = str(photo['face_count'])
                            elif key == 'tags':
                                # Use shared tag buttons frame for compact view
                                create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], existing_tags, use_grid=True, row=0, col=col_idx)
                                col_idx += 1
                                continue
                            
                            ttk.Label(row_frame, text=text).grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                            col_idx += 1
                        
                        current_row += 1
        
        def switch_view_mode(mode):
            if mode == "list":
                show_list_view()
            elif mode == "icons":
                show_icon_view()
            elif mode == "compact":
                show_compact_view()
        
        # No need for canvas resize handler since icon view is now single column
        
        # Load initial data and show default view
        load_existing_tags()
        load_photos()
        show_list_view()
        
        # Show window
        root.deiconify()
        root.mainloop()
        
        return 0

    def modifyidentified(self) -> int:
        """Modify identified faces interface - empty window with Quit button for now"""
        import tkinter as tk
        from tkinter import ttk, messagebox
        from PIL import Image, ImageTk
        import os
        
        # Simple tooltip implementation
        class ToolTip:
            def __init__(self, widget, text):
                self.widget = widget
                self.text = text
                self.tooltip_window = None
                self.widget.bind("<Enter>", self.on_enter)
                self.widget.bind("<Leave>", self.on_leave)
            
            def on_enter(self, event=None):
                if self.tooltip_window or not self.text:
                    return
                x, y, _, _ = self.widget.bbox("insert") if hasattr(self.widget, 'bbox') else (0, 0, 0, 0)
                x += self.widget.winfo_rootx() + 25
                y += self.widget.winfo_rooty() + 25
                
                self.tooltip_window = tw = tk.Toplevel(self.widget)
                tw.wm_overrideredirect(True)
                tw.wm_geometry(f"+{x}+{y}")
                
                label = tk.Label(tw, text=self.text, justify=tk.LEFT,
                               background="#ffffe0", relief=tk.SOLID, borderwidth=1,
                               font=("tahoma", "8", "normal"))
                label.pack(ipadx=1)
            
            def on_leave(self, event=None):
                if self.tooltip_window:
                    self.tooltip_window.destroy()
                    self.tooltip_window = None
        
        # Create the main window
        root = tk.Tk()
        root.title("View and Modify Identified Faces")
        root.resizable(True, True)
        
        # Track window state to prevent multiple destroy calls
        window_destroyed = False
        temp_crops = []
        right_panel_images = []  # Keep PhotoImage refs alive
        selected_person_id = None
        
        # 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
            # Cleanup temp crops
            for crop in list(temp_crops):
                try:
                    if os.path.exists(crop):
                        os.remove(crop)
                except:
                    pass
            temp_crops.clear()
            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
        saved_size = self._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)
        main_frame.columnconfigure(1, weight=2)
        main_frame.rowconfigure(1, weight=1)
        
        # Title label
        title_label = ttk.Label(main_frame, text="View and Modify Identified Faces", font=("Arial", 16, "bold"))
        title_label.grid(row=0, column=0, columnspan=2, pady=(0, 10), sticky=tk.W)
        
        # Left panel: People list
        people_frame = ttk.LabelFrame(main_frame, text="People", padding="10")
        people_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 8))
        people_frame.columnconfigure(0, weight=1)

        # Search controls (Last Name) with label under the input (match auto-match style)
        last_name_search_var = tk.StringVar()
        search_frame = ttk.Frame(people_frame)
        search_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 6))
        
        # Entry 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 to the right of the entry
        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 entry
        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))

        people_canvas = tk.Canvas(people_frame, bg='white')
        people_scrollbar = ttk.Scrollbar(people_frame, orient="vertical", command=people_canvas.yview)
        people_list_inner = ttk.Frame(people_canvas)
        people_canvas.create_window((0, 0), window=people_list_inner, anchor="nw")
        people_canvas.configure(yscrollcommand=people_scrollbar.set)
        
        people_list_inner.bind(
            "<Configure>",
            lambda e: people_canvas.configure(scrollregion=people_canvas.bbox("all"))
        )
        
        people_canvas.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        people_scrollbar.grid(row=1, column=1, sticky=(tk.N, tk.S))
        people_frame.rowconfigure(1, weight=1)
        
        # Right panel: Faces for selected person
        faces_frame = ttk.LabelFrame(main_frame, text="Faces", padding="10")
        faces_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
        faces_frame.columnconfigure(0, weight=1)
        faces_frame.rowconfigure(0, weight=1)
        
        style = ttk.Style()
        canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
        # Match auto-match UI: set gray background for left canvas and remove highlight border
        try:
            people_canvas.configure(bg=canvas_bg_color, highlightthickness=0)
        except Exception:
            pass
        faces_canvas = tk.Canvas(faces_frame, bg=canvas_bg_color, highlightthickness=0)
        faces_scrollbar = ttk.Scrollbar(faces_frame, orient="vertical", command=faces_canvas.yview)
        faces_inner = ttk.Frame(faces_canvas)
        faces_canvas.create_window((0, 0), window=faces_inner, anchor="nw")
        faces_canvas.configure(yscrollcommand=faces_scrollbar.set)
        
        faces_inner.bind(
            "<Configure>",
            lambda e: faces_canvas.configure(scrollregion=faces_canvas.bbox("all"))
        )
        
        # Track current person for responsive face grid
        current_person_id = None
        current_person_name = ""
        resize_job = None
        
        # Track unmatched faces (temporary changes)
        unmatched_faces = set()  # All face IDs unmatched across people (for global save)
        unmatched_by_person = {}  # person_id -> set(face_id) for per-person undo
        original_faces_data = []  # store original faces data for potential future use

        def on_faces_canvas_resize(event):
            nonlocal resize_job
            if current_person_id is None:
                return
            # Debounce re-render on resize
            try:
                if resize_job is not None:
                    root.after_cancel(resize_job)
            except Exception:
                pass
            resize_job = root.after(150, lambda: show_person_faces(current_person_id, current_person_name))

        faces_canvas.bind("<Configure>", on_faces_canvas_resize)

        faces_canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        faces_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        
        # Load people from DB with counts
        people_data = []  # list of dicts: {id, name, count, first_name, last_name}
        people_filtered = None  # filtered subset based on last name search
        
        def load_people():
            nonlocal people_data
            with self.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute(
                    """
                    SELECT p.id, p.first_name, p.last_name, p.middle_name, p.maiden_name, p.date_of_birth, COUNT(f.id) as face_count
                    FROM people p
                    JOIN faces f ON f.person_id = p.id
                    GROUP BY p.id, p.last_name, p.first_name, p.middle_name, p.maiden_name, p.date_of_birth
                    HAVING face_count > 0
                    ORDER BY p.last_name, p.first_name COLLATE NOCASE
                    """
                )
                people_data = []
                for (pid, first_name, last_name, middle_name, maiden_name, date_of_birth, count) in cursor.fetchall():
                    # Create full name display with all available information
                    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) if name_parts else "Unknown"
                    
                    # Create detailed display with date of birth if available
                    display_name = full_name
                    if date_of_birth:
                        display_name += f" - Born: {date_of_birth}"
                    
                    people_data.append({
                        'id': pid,
                        'name': display_name,
                        'full_name': full_name,
                        'first_name': first_name or "",
                        'last_name': last_name or "",
                        'middle_name': middle_name or "",
                        'maiden_name': maiden_name or "",
                        'date_of_birth': date_of_birth or "",
                        'count': count
                    })
            # Re-apply filter (if any) after loading
            try:
                apply_last_name_filter()
            except Exception:
                pass

        # 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

        def apply_last_name_filter():
            nonlocal people_filtered
            query = last_name_search_var.get().strip().lower()
            if query:
                people_filtered = [p for p in people_data if p.get('last_name', '').lower().find(query) != -1]
            else:
                people_filtered = None
            populate_people_list()
            # Update right panel based on filtered results
            source = people_filtered if people_filtered is not None else people_data
            if source:
                # Load faces for the first person in the list
                first = source[0]
                try:
                    # Update selection state
                    for child in people_list_inner.winfo_children():
                        for widget in child.winfo_children():
                            if isinstance(widget, ttk.Label):
                                widget.config(font=("Arial", 10))
                    # Bold the first label if present
                    first_row = people_list_inner.winfo_children()[0]
                    for widget in first_row.winfo_children():
                        if isinstance(widget, ttk.Label):
                            widget.config(font=("Arial", 10, "bold"))
                            break
                except Exception:
                    pass
                # Show faces for the first person
                show_person_faces(first['id'], first.get('full_name') or first.get('name') or "")
            else:
                # No matches: clear faces panel
                clear_faces_panel()

        def clear_last_name_filter():
            nonlocal people_filtered
            last_name_search_var.set("")
            people_filtered = None
            populate_people_list()
            # After clearing, load faces for the first available person if any
            if people_data:
                first = people_data[0]
                try:
                    for child in people_list_inner.winfo_children():
                        for widget in child.winfo_children():
                            if isinstance(widget, ttk.Label):
                                widget.config(font=("Arial", 10))
                    first_row = people_list_inner.winfo_children()[0]
                    for widget in first_row.winfo_children():
                        if isinstance(widget, ttk.Label):
                            widget.config(font=("Arial", 10, "bold"))
                            break
                except Exception:
                    pass
                show_person_faces(first['id'], first.get('full_name') or first.get('name') or "")
            else:
                clear_faces_panel()
        
        def clear_faces_panel():
            for w in faces_inner.winfo_children():
                w.destroy()
            # Cleanup temp crops
            for crop in list(temp_crops):
                try:
                    if os.path.exists(crop):
                        os.remove(crop)
                except:
                    pass
            temp_crops.clear()
            right_panel_images.clear()
        
        def unmatch_face(face_id: int):
            """Temporarily unmatch a face from the current person"""
            nonlocal unmatched_faces, unmatched_by_person
            unmatched_faces.add(face_id)
            # Track per-person for Undo
            person_set = unmatched_by_person.get(current_person_id)
            if person_set is None:
                person_set = set()
                unmatched_by_person[current_person_id] = person_set
            person_set.add(face_id)
            # Refresh the display
            show_person_faces(current_person_id, current_person_name)
        
        def undo_changes():
            """Undo all temporary changes"""
            nonlocal unmatched_faces, unmatched_by_person
            if current_person_id in unmatched_by_person:
                for fid in list(unmatched_by_person[current_person_id]):
                    unmatched_faces.discard(fid)
                unmatched_by_person[current_person_id].clear()
            # Refresh the display
            show_person_faces(current_person_id, current_person_name)
        
        def save_changes():
            """Save unmatched faces to database"""
            if not unmatched_faces:
                return
            
            # Confirm with user
            result = messagebox.askyesno(
                "Confirm Changes",
                f"Are you sure you want to unlink {len(unmatched_faces)} face(s) from '{current_person_name}'?\n\n"
                "This will make these faces unidentified again."
            )
            
            if not result:
                return
            
            # Update database
            with self.get_db_connection() as conn:
                cursor = conn.cursor()
                for face_id in unmatched_faces:
                    cursor.execute('UPDATE faces SET person_id = NULL WHERE id = ?', (face_id,))
                conn.commit()
            
            # Store count for message before clearing
            unlinked_count = len(unmatched_faces)
            
            # Clear unmatched faces and refresh
            unmatched_faces.clear()
            original_faces_data.clear()
            
            # Refresh people list to update counts
            load_people()
            populate_people_list()
            
            # Refresh faces display
            show_person_faces(current_person_id, current_person_name)
            
            messagebox.showinfo("Changes Saved", f"Successfully unlinked {unlinked_count} face(s) from '{current_person_name}'.")
        
        def show_person_faces(person_id: int, person_name: str):
            nonlocal current_person_id, current_person_name, unmatched_faces, original_faces_data
            current_person_id = person_id
            current_person_name = person_name
            clear_faces_panel()
            
            # Determine how many columns fit the available width
            available_width = faces_canvas.winfo_width()
            if available_width <= 1:
                available_width = faces_frame.winfo_width()
            tile_width = 150  # approx tile + padding
            cols = max(1, available_width // tile_width)
            
            # Header row
            header = ttk.Label(faces_inner, text=f"Faces for: {person_name}", font=("Arial", 12, "bold"))
            header.grid(row=0, column=0, columnspan=cols, sticky=tk.W, pady=(0, 5))
            
            # Control buttons row
            button_frame = ttk.Frame(faces_inner)
            button_frame.grid(row=1, column=0, columnspan=cols, sticky=tk.W, pady=(0, 10))
            
            # Enable Undo only if current person has unmatched faces
            current_has_unmatched = bool(unmatched_by_person.get(current_person_id))
            undo_btn = ttk.Button(button_frame, text="↶ Undo changes", 
                                command=lambda: undo_changes(),
                                state="disabled" if not current_has_unmatched else "normal")
            undo_btn.pack(side=tk.LEFT, padx=(0, 10))
            
            # Note: Save button moved to bottom control bar
            
            # Query faces for this person
            with self.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute(
                    """
                    SELECT f.id, f.location, ph.path, ph.filename
                    FROM faces f
                    JOIN photos ph ON ph.id = f.photo_id
                    WHERE f.person_id = ?
                    ORDER BY f.id DESC
                    """,
                    (person_id,)
                )
                rows = cursor.fetchall()
            
            # Filter out unmatched faces
            visible_rows = [row for row in rows if row[0] not in unmatched_faces]
            
            if not visible_rows:
                ttk.Label(faces_inner, text="No faces found." if not rows else "All faces unmatched.", foreground="gray").grid(row=2, column=0, sticky=tk.W)
                return
            
            # Grid thumbnails with responsive column count
            row_index = 2  # Start after header and buttons
            col_index = 0
            for face_id, location, photo_path, filename in visible_rows:
                crop_path = self._extract_face_crop(photo_path, location, face_id)
                thumb = None
                if crop_path and os.path.exists(crop_path):
                    try:
                        img = Image.open(crop_path)
                        img.thumbnail((130, 130), Image.Resampling.LANCZOS)
                        photo_img = ImageTk.PhotoImage(img)
                        temp_crops.append(crop_path)
                        right_panel_images.append(photo_img)
                        thumb = photo_img
                    except Exception:
                        thumb = None
                
                tile = ttk.Frame(faces_inner, padding="5")
                tile.grid(row=row_index, column=col_index, padx=5, pady=5, sticky=tk.N)
                
                # Create a frame for the face image with X button overlay
                face_frame = ttk.Frame(tile)
                face_frame.grid(row=0, column=0)
                
                canvas = tk.Canvas(face_frame, width=130, height=130, bg=canvas_bg_color, highlightthickness=0)
                canvas.grid(row=0, column=0)
                if thumb is not None:
                    canvas.create_image(65, 65, image=thumb)
                else:
                    canvas.create_text(65, 65, text="🖼️", fill="gray")
                
                # X button to unmatch face - pin exactly to the canvas' top-right corner
                x_canvas = tk.Canvas(face_frame, width=12, height=12, bg='red', 
                                    highlightthickness=0, relief="flat")
                x_canvas.create_text(6, 6, text="✖", fill="white", font=("Arial", 8, "bold"))
                # Click handler
                x_canvas.bind("<Button-1>", lambda e, fid=face_id: unmatch_face(fid))
                # Hover highlight: change bg, show white outline, and hand cursor
                x_canvas.bind("<Enter>", lambda e, c=x_canvas: c.configure(bg="#cc0000", highlightthickness=1, highlightbackground="white", cursor="hand2"))
                x_canvas.bind("<Leave>", lambda e, c=x_canvas: c.configure(bg="red", highlightthickness=0, cursor=""))
                # Anchor to the canvas' top-right regardless of layout/size
                try:
                    x_canvas.place(in_=canvas, relx=1.0, x=0, y=0, anchor='ne')
                except Exception:
                    # Fallback to absolute coords if relative placement fails
                    x_canvas.place(x=118, y=0)
                
                ttk.Label(tile, text=f"ID {face_id}", foreground="gray").grid(row=1, column=0)
                
                col_index += 1
                if col_index >= cols:
                    col_index = 0
                    row_index += 1
        
        def populate_people_list():
            for w in people_list_inner.winfo_children():
                w.destroy()
            source = people_filtered if people_filtered is not None else people_data
            if not source:
                empty_label = ttk.Label(people_list_inner, text="No people match filter", foreground="gray")
                empty_label.grid(row=0, column=0, sticky=tk.W, pady=4)
                return
            for idx, person in enumerate(source):
                row = ttk.Frame(people_list_inner)
                row.grid(row=idx, column=0, sticky=(tk.W, tk.E), pady=4)
                # Freeze per-row values to avoid late-binding issues
                row_person = person
                row_idx = idx
                
                # Make person name clickable
                def make_click_handler(p_id, p_name, p_idx):
                    def on_click(event):
                        nonlocal selected_person_id
                        # Reset all labels to normal font
                        for child in people_list_inner.winfo_children():
                            for widget in child.winfo_children():
                                if isinstance(widget, ttk.Label):
                                    widget.config(font=("Arial", 10))
                        # Set clicked label to bold
                        event.widget.config(font=("Arial", 10, "bold"))
                        selected_person_id = p_id
                        # Show faces for this person
                        show_person_faces(p_id, p_name)
                    return on_click
                
                
                # Edit (rename) button
                def start_edit_person(row_frame, person_record, row_index):
                    for w in row_frame.winfo_children():
                        w.destroy()
                    
                    # Use pre-loaded data instead of database query
                    cur_first = person_record.get('first_name', '')
                    cur_last = person_record.get('last_name', '')
                    cur_middle = person_record.get('middle_name', '')
                    cur_maiden = person_record.get('maiden_name', '')
                    cur_dob = person_record.get('date_of_birth', '')

                    # Create a larger container frame for the text boxes and labels
                    edit_container = ttk.Frame(row_frame)
                    edit_container.pack(side=tk.LEFT, fill=tk.X, expand=True)
                    
                    # Create a grid layout for better organization
                    # First name field with label
                    first_frame = ttk.Frame(edit_container)
                    first_frame.grid(row=0, column=0, padx=(0, 10), pady=(0, 5), sticky=tk.W)
                    
                    first_var = tk.StringVar(value=cur_first)
                    first_entry = ttk.Entry(first_frame, textvariable=first_var, width=15)
                    first_entry.pack(side=tk.TOP)
                    first_entry.focus_set()
                    
                    first_label = ttk.Label(first_frame, text="First Name", font=("Arial", 8), foreground="gray")
                    first_label.pack(side=tk.TOP, pady=(2, 0))
                    
                    # Last name field with label
                    last_frame = ttk.Frame(edit_container)
                    last_frame.grid(row=0, column=1, padx=(0, 10), pady=(0, 5), sticky=tk.W)
                    
                    last_var = tk.StringVar(value=cur_last)
                    last_entry = ttk.Entry(last_frame, textvariable=last_var, width=15)
                    last_entry.pack(side=tk.TOP)
                    
                    last_label = ttk.Label(last_frame, text="Last Name", font=("Arial", 8), foreground="gray")
                    last_label.pack(side=tk.TOP, pady=(2, 0))
                    
                    # Middle name field with label
                    middle_frame = ttk.Frame(edit_container)
                    middle_frame.grid(row=0, column=2, padx=(0, 10), pady=(0, 5), sticky=tk.W)
                    
                    middle_var = tk.StringVar(value=cur_middle)
                    middle_entry = ttk.Entry(middle_frame, textvariable=middle_var, width=15)
                    middle_entry.pack(side=tk.TOP)
                    
                    middle_label = ttk.Label(middle_frame, text="Middle Name", font=("Arial", 8), foreground="gray")
                    middle_label.pack(side=tk.TOP, pady=(2, 0))
                    
                    # Maiden name field with label
                    maiden_frame = ttk.Frame(edit_container)
                    maiden_frame.grid(row=1, column=0, padx=(0, 10), pady=(0, 5), sticky=tk.W)
                    
                    maiden_var = tk.StringVar(value=cur_maiden)
                    maiden_entry = ttk.Entry(maiden_frame, textvariable=maiden_var, width=15)
                    maiden_entry.pack(side=tk.TOP)
                    
                    maiden_label = ttk.Label(maiden_frame, text="Maiden Name", font=("Arial", 8), foreground="gray")
                    maiden_label.pack(side=tk.TOP, pady=(2, 0))
                    
                    # Date of birth field with label and calendar button
                    dob_frame = ttk.Frame(edit_container)
                    dob_frame.grid(row=1, column=1, columnspan=2, padx=(0, 10), pady=(0, 5), sticky=tk.W)
                    
                    # Create a frame for the date picker
                    date_picker_frame = ttk.Frame(dob_frame)
                    date_picker_frame.pack(side=tk.TOP)
                    
                    dob_var = tk.StringVar(value=cur_dob)
                    dob_entry = ttk.Entry(date_picker_frame, textvariable=dob_var, width=20, state='readonly')
                    dob_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
                    
                    # Calendar button
                    calendar_btn = ttk.Button(date_picker_frame, text="📅", width=3, command=lambda: open_calendar())
                    calendar_btn.pack(side=tk.RIGHT, padx=(5, 0))
                    
                    dob_label = ttk.Label(dob_frame, text="Date of Birth", font=("Arial", 8), foreground="gray")
                    dob_label.pack(side=tk.TOP, pady=(2, 0))
                    
                    def open_calendar():
                        """Open a visual calendar dialog to select date of birth"""
                        from datetime import datetime, date, timedelta
                        import calendar
                        
                        # Create calendar window
                        calendar_window = tk.Toplevel(root)
                        calendar_window.title("Select Date of Birth")
                        calendar_window.resizable(False, False)
                        calendar_window.transient(root)
                        calendar_window.grab_set()
                        
                        # Calculate center position before showing the window
                        window_width = 400
                        window_height = 400
                        screen_width = calendar_window.winfo_screenwidth()
                        screen_height = calendar_window.winfo_screenheight()
                        x = (screen_width // 2) - (window_width // 2)
                        y = (screen_height // 2) - (window_height // 2)
                        
                        # Set geometry with center position before showing
                        calendar_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
                        
                        # Calendar variables
                        current_date = datetime.now()
                        
                        # Check if there's already a date selected
                        existing_date_str = dob_var.get().strip()
                        if existing_date_str:
                            try:
                                existing_date = datetime.strptime(existing_date_str, '%Y-%m-%d').date()
                                display_year = existing_date.year
                                display_month = existing_date.month
                                selected_date = existing_date
                            except ValueError:
                                # If existing date is invalid, use default
                                display_year = current_date.year - 25
                                display_month = 1
                                selected_date = None
                        else:
                            # Default to 25 years ago
                            display_year = current_date.year - 25
                            display_month = 1
                            selected_date = None
                        
                        # Month names
                        month_names = ["January", "February", "March", "April", "May", "June",
                                      "July", "August", "September", "October", "November", "December"]
                        
                        # Configure custom styles for better visual highlighting
                        style = ttk.Style()
                        
                        # Selected date style - bright blue background with white text
                        style.configure("Selected.TButton", 
                                      background="#0078d4", 
                                      foreground="white",
                                      font=("Arial", 9, "bold"),
                                      relief="raised",
                                      borderwidth=2)
                        style.map("Selected.TButton",
                                 background=[("active", "#106ebe")],
                                 relief=[("pressed", "sunken")])
                        
                        # Today's date style - orange background
                        style.configure("Today.TButton",
                                      background="#ff8c00",
                                      foreground="white", 
                                      font=("Arial", 9, "bold"),
                                      relief="raised",
                                      borderwidth=1)
                        style.map("Today.TButton",
                                 background=[("active", "#e67e00")],
                                 relief=[("pressed", "sunken")])
                        
                        # Calendar-specific normal button style (don't affect global TButton)
                        style.configure("Calendar.TButton",
                                      font=("Arial", 9),
                                      relief="flat")
                        style.map("Calendar.TButton",
                                 background=[("active", "#e1e1e1")],
                                 relief=[("pressed", "sunken")])
                        
                        # Main frame
                        main_cal_frame = ttk.Frame(calendar_window, padding="10")
                        main_cal_frame.pack(fill=tk.BOTH, expand=True)
                        
                        # Header frame with navigation
                        header_frame = ttk.Frame(main_cal_frame)
                        header_frame.pack(fill=tk.X, pady=(0, 10))
                        
                        # Month/Year display and navigation
                        nav_frame = ttk.Frame(header_frame)
                        nav_frame.pack()
                        
                        def update_calendar():
                            """Update the calendar display"""
                            # Clear existing calendar
                            for widget in calendar_frame.winfo_children():
                                widget.destroy()
                            
                            # Update header
                            month_year_label.config(text=f"{month_names[display_month-1]} {display_year}")
                            
                            # Get calendar data
                            cal = calendar.monthcalendar(display_year, display_month)
                            
                            # Day headers
                            day_headers = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
                            for i, day in enumerate(day_headers):
                                label = ttk.Label(calendar_frame, text=day, font=("Arial", 9, "bold"))
                                label.grid(row=0, column=i, padx=1, pady=1, sticky="nsew")
                            
                            # Calendar days
                            for week_num, week in enumerate(cal):
                                for day_num, day in enumerate(week):
                                    if day == 0:
                                        # Empty cell
                                        label = ttk.Label(calendar_frame, text="")
                                        label.grid(row=week_num+1, column=day_num, padx=1, pady=1, sticky="nsew")
                                    else:
                                        # Day button
                                        def make_day_handler(day_value):
                                            def select_day():
                                                nonlocal selected_date
                                                selected_date = date(display_year, display_month, day_value)
                                                # Reset all buttons to normal calendar style
                                                for widget in calendar_frame.winfo_children():
                                                    if isinstance(widget, ttk.Button):
                                                        widget.config(style="Calendar.TButton")
                                                # Highlight selected day with prominent style
                                                for widget in calendar_frame.winfo_children():
                                                    if isinstance(widget, ttk.Button) and widget.cget("text") == str(day_value):
                                                        widget.config(style="Selected.TButton")
                                            return select_day
                                        
                                        day_btn = ttk.Button(calendar_frame, text=str(day), 
                                                           command=make_day_handler(day),
                                                           width=3, style="Calendar.TButton")
                                        day_btn.grid(row=week_num+1, column=day_num, padx=1, pady=1, sticky="nsew")
                                        
                                        # Check if this day should be highlighted
                                        is_today = (display_year == current_date.year and 
                                                   display_month == current_date.month and 
                                                   day == current_date.day)
                                        is_selected = (selected_date and 
                                                     selected_date.year == display_year and 
                                                     selected_date.month == display_month and 
                                                     selected_date.day == day)
                                        
                                        if is_selected:
                                            day_btn.config(style="Selected.TButton")
                                        elif is_today:
                                            day_btn.config(style="Today.TButton")
                        
                        # Navigation functions
                        def prev_year():
                            nonlocal display_year
                            display_year = max(1900, display_year - 1)
                            update_calendar()
                        
                        def next_year():
                            nonlocal display_year
                            display_year = min(current_date.year, display_year + 1)
                            update_calendar()
                        
                        def prev_month():
                            nonlocal display_month, display_year
                            if display_month > 1:
                                display_month -= 1
                            else:
                                display_month = 12
                                display_year = max(1900, display_year - 1)
                            update_calendar()
                        
                        def next_month():
                            nonlocal display_month, display_year
                            if display_month < 12:
                                display_month += 1
                            else:
                                display_month = 1
                                display_year = min(current_date.year, display_year + 1)
                            update_calendar()
                        
                        # Navigation buttons
                        prev_year_btn = ttk.Button(nav_frame, text="<<", width=3, command=prev_year)
                        prev_year_btn.pack(side=tk.LEFT, padx=(0, 2))
                        
                        prev_month_btn = ttk.Button(nav_frame, text="<", width=3, command=prev_month)
                        prev_month_btn.pack(side=tk.LEFT, padx=(0, 5))
                        
                        month_year_label = ttk.Label(nav_frame, text="", font=("Arial", 12, "bold"))
                        month_year_label.pack(side=tk.LEFT, padx=5)
                        
                        next_month_btn = ttk.Button(nav_frame, text=">", width=3, command=next_month)
                        next_month_btn.pack(side=tk.LEFT, padx=(5, 2))
                        
                        next_year_btn = ttk.Button(nav_frame, text=">>", width=3, command=next_year)
                        next_year_btn.pack(side=tk.LEFT)
                        
                        # Calendar grid frame
                        calendar_frame = ttk.Frame(main_cal_frame)
                        calendar_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
                        
                        # Configure grid weights
                        for i in range(7):
                            calendar_frame.columnconfigure(i, weight=1)
                        for i in range(7):
                            calendar_frame.rowconfigure(i, weight=1)
                        
                        # Buttons frame
                        buttons_frame = ttk.Frame(main_cal_frame)
                        buttons_frame.pack(fill=tk.X)
                        
                        def select_date():
                            """Select the date and close calendar"""
                            if selected_date:
                                date_str = selected_date.strftime('%Y-%m-%d')
                                dob_var.set(date_str)
                                calendar_window.destroy()
                            else:
                                messagebox.showwarning("No Date Selected", "Please select a date from the calendar.")
                        
                        def cancel_selection():
                            """Cancel date selection"""
                            calendar_window.destroy()
                        
                        # Buttons
                        ttk.Button(buttons_frame, text="Select", command=select_date).pack(side=tk.LEFT, padx=(0, 5))
                        ttk.Button(buttons_frame, text="Cancel", command=cancel_selection).pack(side=tk.LEFT)
                        
                        # Initialize calendar
                        update_calendar()
                    
                    def save_rename():
                        new_first = first_var.get().strip()
                        new_last = last_var.get().strip()
                        new_middle = middle_var.get().strip()
                        new_maiden = maiden_var.get().strip()
                        new_dob = dob_var.get().strip()
                        
                        if not new_first and not new_last:
                            messagebox.showwarning("Invalid name", "At least one of First or Last name must be provided.")
                            return
                        
                        # Check for duplicates in local data first (based on first and last name only)
                        for person in people_data:
                            if person['id'] != person_record['id'] and person['first_name'] == new_first and person['last_name'] == new_last:
                                display_name = f"{new_last}, {new_first}".strip(", ").strip()
                                messagebox.showwarning("Duplicate name", f"A person named '{display_name}' already exists.")
                                return
                        
                        # Single database access - save to database
                        with self.get_db_connection() as conn:
                            cursor = conn.cursor()
                            cursor.execute('UPDATE people SET first_name = ?, last_name = ?, middle_name = ?, maiden_name = ?, date_of_birth = ? WHERE id = ?', 
                                         (new_first, new_last, new_middle, new_maiden, new_dob, person_record['id']))
                            conn.commit()
                        
                        # Update local data structure
                        person_record['first_name'] = new_first
                        person_record['last_name'] = new_last
                        person_record['middle_name'] = new_middle
                        person_record['maiden_name'] = new_maiden
                        person_record['date_of_birth'] = new_dob
                        
                        # Recreate the full display name with all available information
                        name_parts = []
                        if new_first:
                            name_parts.append(new_first)
                        if new_middle:
                            name_parts.append(new_middle)
                        if new_last:
                            name_parts.append(new_last)
                        if new_maiden:
                            name_parts.append(f"({new_maiden})")
                        
                        full_name = ' '.join(name_parts) if name_parts else "Unknown"
                        
                        # Create detailed display with date of birth if available
                        display_name = full_name
                        if new_dob:
                            display_name += f" - Born: {new_dob}"
                        
                        person_record['name'] = display_name
                        person_record['full_name'] = full_name
                        
                        # Refresh list
                        current_selected_id = person_record['id']
                        populate_people_list()
                        # Reselect and refresh right panel header if needed
                        if selected_person_id == current_selected_id or selected_person_id is None:
                            # Find updated name
                            updated = next((p for p in people_data if p['id'] == current_selected_id), None)
                            if updated:
                                # Bold corresponding label
                                for child in people_list_inner.winfo_children():
                                    # child is row frame: contains label and button
                                    widgets = child.winfo_children()
                                    if not widgets:
                                        continue
                                    lbl = widgets[0]
                                    if isinstance(lbl, ttk.Label) and lbl.cget('text').startswith(updated['name'] + " ("):
                                        lbl.config(font=("Arial", 10, "bold"))
                                        break
                                # Update right panel header by re-showing faces
                                show_person_faces(updated['id'], updated['name'])
                        
                    def cancel_edit():
                        # Rebuild the row back to label + edit
                        for w in row_frame.winfo_children():
                            w.destroy()
                        rebuild_row(row_frame, person_record, row_index)
                    
                    save_btn = ttk.Button(row_frame, text="💾", width=3, command=save_rename)
                    save_btn.pack(side=tk.LEFT, padx=(5, 0))
                    cancel_btn = ttk.Button(row_frame, text="✖", width=3, command=cancel_edit)
                    cancel_btn.pack(side=tk.LEFT, padx=(5, 0))
                    
                    # Configure custom disabled button style for better visibility
                    style = ttk.Style()
                    style.configure("Disabled.TButton",
                                  background="#d3d3d3",  # Light gray background
                                  foreground="#808080",  # Dark gray text
                                  relief="flat",
                                  borderwidth=1)
                    
                    def validate_save_button():
                        """Enable/disable save button based on required fields"""
                        first_val = first_var.get().strip()
                        last_val = last_var.get().strip()
                        dob_val = dob_var.get().strip()
                        
                        # Enable save button only if both name fields and date of birth are provided
                        has_first = bool(first_val)
                        has_last = bool(last_val)
                        has_dob = bool(dob_val)
                        
                        if has_first and has_last and has_dob:
                            save_btn.config(state="normal")
                            # Reset to normal styling when enabled
                            save_btn.config(style="TButton")
                        else:
                            save_btn.config(state="disabled")
                            # Apply custom disabled styling for better visibility
                            save_btn.config(style="Disabled.TButton")
                    
                    # Set up validation callbacks for all input fields
                    first_var.trace('w', lambda *args: validate_save_button())
                    last_var.trace('w', lambda *args: validate_save_button())
                    middle_var.trace('w', lambda *args: validate_save_button())
                    maiden_var.trace('w', lambda *args: validate_save_button())
                    dob_var.trace('w', lambda *args: validate_save_button())
                    
                    # Initial validation
                    validate_save_button()
                    
                    # Keyboard shortcuts (only work when save button is enabled)
                    def try_save():
                        if save_btn.cget('state') == 'normal':
                            save_rename()
                    
                    first_entry.bind('<Return>', lambda e: try_save())
                    last_entry.bind('<Return>', lambda e: try_save())
                    middle_entry.bind('<Return>', lambda e: try_save())
                    maiden_entry.bind('<Return>', lambda e: try_save())
                    dob_entry.bind('<Return>', lambda e: try_save())
                    first_entry.bind('<Escape>', lambda e: cancel_edit())
                    last_entry.bind('<Escape>', lambda e: cancel_edit())
                    middle_entry.bind('<Escape>', lambda e: cancel_edit())
                    maiden_entry.bind('<Escape>', lambda e: cancel_edit())
                    dob_entry.bind('<Escape>', lambda e: cancel_edit())
                
                def rebuild_row(row_frame, p, i):
                    # Edit button (on the left)
                    edit_btn = ttk.Button(row_frame, text="✏️", width=3, command=lambda r=row_frame, pr=p, ii=i: start_edit_person(r, pr, ii))
                    edit_btn.pack(side=tk.LEFT, padx=(0, 5))
                    # Add tooltip to edit button
                    ToolTip(edit_btn, "Update name")
                    # Label (clickable) - takes remaining space
                    name_lbl = ttk.Label(row_frame, text=f"{p['name']} ({p['count']})", font=("Arial", 10))
                    name_lbl.pack(side=tk.LEFT, fill=tk.X, expand=True)
                    name_lbl.bind("<Button-1>", make_click_handler(p['id'], p['name'], i))
                    name_lbl.config(cursor="hand2")
                    # Bold if selected
                    if (selected_person_id is None and i == 0) or (selected_person_id == p['id']):
                        name_lbl.config(font=("Arial", 10, "bold"))
                
                # Build row contents with edit button
                rebuild_row(row, row_person, row_idx)
        
        # Initial load
        load_people()
        populate_people_list()
        
        # Show first person's faces by default and mark selected
        if people_data:
            selected_person_id = people_data[0]['id']
            show_person_faces(people_data[0]['id'], people_data[0]['name'])
        
        # Control buttons
        control_frame = ttk.Frame(main_frame)
        control_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0), sticky=tk.E)
        
        def on_quit():
            nonlocal window_destroyed
            on_closing()
            if not window_destroyed:
                window_destroyed = True
                try:
                    root.destroy()
                except tk.TclError:
                    pass  # Window already destroyed
        
        def on_save_all_changes():
            # Use global unmatched_faces set; commit all across people
            nonlocal unmatched_faces
            if not unmatched_faces:
                messagebox.showinfo("Nothing to Save", "There are no pending changes to save.")
                return
            result = messagebox.askyesno(
                "Confirm Save",
                f"Unlink {len(unmatched_faces)} face(s) across all people?\n\nThis will make them unidentified."
            )
            if not result:
                return
            with self.get_db_connection() as conn:
                cursor = conn.cursor()
                for face_id in unmatched_faces:
                    cursor.execute('UPDATE faces SET person_id = NULL WHERE id = ?', (face_id,))
                conn.commit()
            count = len(unmatched_faces)
            unmatched_faces.clear()
            # Refresh people list and right panel for current selection
            load_people()
            populate_people_list()
            if current_person_id is not None and current_person_name:
                show_person_faces(current_person_id, current_person_name)
            messagebox.showinfo("Changes Saved", f"Successfully unlinked {count} face(s).")

        save_btn_bottom = ttk.Button(control_frame, text="💾 Save changes", command=on_save_all_changes)
        save_btn_bottom.pack(side=tk.RIGHT, padx=(0, 10))
        quit_btn = ttk.Button(control_frame, text="❌ Quit", command=on_quit)
        quit_btn.pack(side=tk.RIGHT)
        
        # Show the window
        try:
            root.deiconify()
            root.lift()
            root.focus_force()
        except tk.TclError:
            # Window was destroyed before we could show it
            return 0
        
        # Main event loop
        try:
            root.mainloop()
        except tk.TclError:
            pass  # Window was destroyed
        
        return 0


def main():
    """Main CLI interface"""
    parser = argparse.ArgumentParser(
        description="PunimTag CLI - Simple photo face tagger",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  photo_tagger.py scan /path/to/photos          # Scan folder for photos
  photo_tagger.py process --limit 20            # Process 20 photos for faces  
  photo_tagger.py identify --batch 10           # Identify 10 faces interactively
  photo_tagger.py auto-match                    # Auto-identify matching faces
  photo_tagger.py modifyidentified              # Show and Modify identified faces
  photo_tagger.py match 15                      # Find faces similar to face ID 15
  photo_tagger.py tag --pattern "vacation"      # Tag photos matching pattern
  photo_tagger.py search "John"                 # Find photos with John
  photo_tagger.py tag-manager                   # Open tag management GUI
  photo_tagger.py stats                         # Show statistics
        """
    )
    
    parser.add_argument('command', 
                       choices=['scan', 'process', 'identify', 'tag', 'search', 'stats', 'match', 'auto-match', 'modifyidentified', 'tag-manager'],
                       help='Command to execute')
    
    parser.add_argument('target', nargs='?', 
                       help='Target folder (scan), person name (search), or pattern (tag)')
    
    parser.add_argument('--db', default='data/photos.db', 
                       help='Database file path (default: data/photos.db)')
    
    parser.add_argument('--limit', type=int, default=50,
                       help='Batch size limit for processing (default: 50)')
    
    parser.add_argument('--batch', type=int, default=20,
                       help='Batch size for identification (default: 20)')
    
    parser.add_argument('--pattern', 
                       help='Pattern for filtering photos when tagging')
    
    parser.add_argument('--model', choices=['hog', 'cnn'], default='hog',
                       help='Face detection model: hog (faster) or cnn (more accurate)')
    
    parser.add_argument('--recursive', action='store_true',
                       help='Scan folders recursively')
    
    parser.add_argument('--show-faces', action='store_true',
                       help='Show individual face crops during identification')
    
    parser.add_argument('--tolerance', type=float, default=0.5,
                       help='Face matching tolerance (0.0-1.0, lower = stricter, default: 0.5)')
    
    parser.add_argument('--auto', action='store_true',
                       help='Auto-identify high-confidence matches without confirmation')
    
    parser.add_argument('--include-twins', action='store_true',
                       help='Include same-photo matching (for twins or multiple instances)')
    
    parser.add_argument('-v', '--verbose', action='count', default=0,
                       help='Increase verbosity (-v, -vv, -vvv for more detail)')
    
    parser.add_argument('--debug', action='store_true',
                       help='Enable line-by-line debugging with pdb')
    
    args = parser.parse_args()
    
    # Initialize tagger
    tagger = PhotoTagger(args.db, args.verbose, args.debug)
    
    try:
        if args.command == 'scan':
            if not args.target:
                print("❌ Please specify a folder to scan")
                return 1
            tagger.scan_folder(args.target, args.recursive)
        
        elif args.command == 'process':
            tagger.process_faces(args.limit, args.model)
        
        elif args.command == 'identify':
            show_faces = getattr(args, 'show_faces', False)
            tagger.identify_faces(args.batch, show_faces, args.tolerance)
        
        elif args.command == 'tag':
            tagger.add_tags(args.pattern or args.target, args.batch)
        
        elif args.command == 'search':
            if not args.target:
                print("❌ Please specify a person name to search for")
                return 1
            tagger.search_faces(args.target)
        
        elif args.command == 'stats':
            tagger.stats()
        
        elif args.command == 'match':
            if args.target and args.target.isdigit():
                face_id = int(args.target)
                matches = tagger.find_similar_faces(face_id, args.tolerance)
                if matches:
                    print(f"\n🎯 Found {len(matches)} similar faces:")
                    for match in matches:
                        person_name = "Unknown" if match['person_id'] is None else f"Person ID {match['person_id']}"
                        print(f"   📸 {match['filename']} - {person_name} (confidence: {(1-match['distance']):.1%})")
                else:
                    print("🔍 No similar faces found")
            else:
                print("❌ Please specify a face ID number to find matches for")
        
        elif args.command == 'auto-match':
            show_faces = getattr(args, 'show_faces', False)
            include_twins = getattr(args, 'include_twins', False)
            tagger.auto_identify_matches(args.tolerance, not args.auto, show_faces, include_twins)
        
        elif args.command == 'modifyidentified':
            tagger.modifyidentified()
        
        elif args.command == 'tag-manager':
            tagger.tag_management()
        
        return 0
        
    except KeyboardInterrupt:
        print("\n\n⚠️  Interrupted by user")
        return 1
    except Exception as e:
        print(f"❌ Error: {e}")
        return 1
    finally:
        # Always cleanup resources
        tagger.cleanup()


if __name__ == "__main__":
    sys.exit(main())
