#!/usr/bin/env python3
"""
Unified Dashboard GUI for PunimTag features
Designed with web migration in mind - single window with menu bar and content area
"""

import os
import threading
import tkinter as tk
from tkinter import ttk, messagebox
from typing import Dict, Optional, Callable

from gui_core import GUICore
from identify_panel import IdentifyPanel
from modify_panel import ModifyPanel
from auto_match_panel import AutoMatchPanel
from search_stats import SearchStats
from database import DatabaseManager
from tag_management import TagManager
from face_processing import FaceProcessor


class SearchPanel:
    """Search panel with full functionality from search_gui.py"""
    
    SEARCH_TYPES = [
        "Search photos by name",
        "Search photos by date",
        "Search photos by tags",
        "Search photos by multiple people (planned)",
        "Most common tags (planned)",
        "Most photographed people (planned)",
        "Photos without faces",
        "Photos without tags",
        "Duplicate faces (planned)",
        "Face quality distribution (planned)",
    ]

    def __init__(self, parent_frame, db_manager: DatabaseManager, search_stats: SearchStats, gui_core: GUICore, tag_manager: TagManager = None, verbose: int = 0):
        self.parent_frame = parent_frame
        self.db = db_manager
        self.search_stats = search_stats
        self.gui_core = gui_core
        self.tag_manager = tag_manager or TagManager(db_manager, verbose)
        self.verbose = verbose
        
        # Sorting state
        self.sort_column = None
        self.sort_reverse = False
        
        # Selection tracking
        self.selected_photos = {}  # photo_path -> photo_data
        
        # Cache for photo tags to avoid database access during updates
        self.photo_tags_cache = {}  # photo_path -> list of tag names

    def create_panel(self) -> ttk.Frame:
        """Create the search panel with all functionality"""
        panel = ttk.Frame(self.parent_frame)
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        # Configure rows: results area (row 3) should expand, buttons (row 4) should not
        panel.rowconfigure(3, weight=1)
        panel.rowconfigure(4, weight=0)
        
        # Search type selector
        type_frame = ttk.Frame(panel)
        type_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        type_frame.columnconfigure(1, weight=1)
        
        ttk.Label(type_frame, text="Search type:", font=("Arial", 10, "bold")).grid(row=0, column=0, sticky=tk.W)
        self.search_type_var = tk.StringVar(value=self.SEARCH_TYPES[0])
        type_combo = ttk.Combobox(type_frame, textvariable=self.search_type_var, values=self.SEARCH_TYPES, state="readonly")
        type_combo.grid(row=0, column=1, padx=(8, 0), sticky=(tk.W, tk.E))

        # Filters area with expand/collapse functionality
        filters_container = ttk.LabelFrame(panel, text="", padding="8")
        filters_container.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        filters_container.columnconfigure(0, weight=1)
        
        # Filters header with toggle text
        filters_header = ttk.Frame(filters_container)
        filters_header.grid(row=0, column=0, sticky=(tk.W, tk.E))
        
        # Toggle text for expand/collapse
        self.filters_expanded = tk.BooleanVar(value=False)  # Start collapsed
        
        def toggle_filters():
            if self.filters_expanded.get():
                # Collapse filters
                filters_content.grid_remove()
                toggle_text.config(text="+")
                self.filters_expanded.set(False)
                update_toggle_tooltip()
            else:
                # Expand filters
                filters_content.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(4, 0))
                toggle_text.config(text="-")
                self.filters_expanded.set(True)
                update_toggle_tooltip()
        
        def update_toggle_tooltip():
            """Update tooltip text based on current state"""
            if self.filters_expanded.get():
                tooltip_text = "Click to collapse filters"
            else:
                tooltip_text = "Click to expand filters"
            toggle_text.tooltip_text = tooltip_text
        
        filters_label = ttk.Label(filters_header, text="Filters", font=("Arial", 10, "bold"))
        filters_label.grid(row=0, column=0, sticky=tk.W)
        
        toggle_text = ttk.Label(filters_header, text="+", font=("Arial", 10, "bold"), cursor="hand2")
        toggle_text.grid(row=0, column=1, padx=(6, 0))
        toggle_text.bind("<Button-1>", lambda e: toggle_filters())
        
        # Initialize tooltip
        toggle_text.tooltip_text = "Click to expand filters"
        update_toggle_tooltip()
        
        # Filters content area (start hidden)
        filters_content = ttk.Frame(filters_container)
        filters_content.columnconfigure(0, weight=1)
        
        # Folder location filter
        folder_filter_frame = ttk.Frame(filters_content)
        folder_filter_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 4))
        folder_filter_frame.columnconfigure(1, weight=1)
        
        ttk.Label(folder_filter_frame, text="Folder location:").grid(row=0, column=0, sticky=tk.W)
        self.folder_var = tk.StringVar()
        folder_entry = ttk.Entry(folder_filter_frame, textvariable=self.folder_var, width=40)
        folder_entry.grid(row=0, column=1, padx=(6, 0), sticky=(tk.W, tk.E))
        ttk.Label(folder_filter_frame, text="(optional - filter by folder path)").grid(row=0, column=2, padx=(6, 0))
        
        # Browse button for folder selection
        def browse_folder():
            from tkinter import filedialog
            from path_utils import normalize_path
            folder_path = filedialog.askdirectory(title="Select folder to filter by")
            if folder_path:
                try:
                    # Normalize to absolute path
                    normalized_path = normalize_path(folder_path)
                    self.folder_var.set(normalized_path)
                except ValueError as e:
                    messagebox.showerror("Invalid Path", f"Invalid folder path: {e}", parent=panel)
        
        browse_btn = ttk.Button(folder_filter_frame, text="Browse", command=browse_folder)
        browse_btn.grid(row=0, column=3, padx=(6, 0))
        
        # Clear folder filter button
        def clear_folder_filter():
            self.folder_var.set("")
        
        clear_folder_btn = ttk.Button(folder_filter_frame, text="Clear", command=clear_folder_filter)
        clear_folder_btn.grid(row=0, column=4, padx=(6, 0))
        
        # Apply filters button
        apply_filters_btn = ttk.Button(filters_content, text="Apply filters", command=lambda: self.do_search())
        apply_filters_btn.grid(row=1, column=0, pady=(8, 0))

        # Inputs area
        inputs = ttk.Frame(panel)
        inputs.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        inputs.columnconfigure(0, weight=1)

        # Name search input
        self.name_frame = ttk.Frame(inputs)
        ttk.Label(self.name_frame, text="Person name:").grid(row=0, column=0, sticky=tk.W)
        self.name_var = tk.StringVar()
        self.name_entry = ttk.Entry(self.name_frame, textvariable=self.name_var)
        self.name_entry.grid(row=0, column=1, padx=(6, 0), sticky=(tk.W, tk.E))
        self.name_frame.columnconfigure(1, weight=1)

        # Tag search input
        self.tag_frame = ttk.Frame(inputs)
        ttk.Label(self.tag_frame, text="Tags:").grid(row=0, column=0, sticky=tk.W)
        self.tag_var = tk.StringVar()
        self.tag_entry = ttk.Entry(self.tag_frame, textvariable=self.tag_var)
        self.tag_entry.grid(row=0, column=1, padx=(6, 0), sticky=(tk.W, tk.E))
        self.tag_frame.columnconfigure(1, weight=1)
        
        # Help icon for available tags
        self.tag_help_icon = ttk.Label(self.tag_frame, text="❓", font=("Arial", 10), cursor="hand2")
        self.tag_help_icon.grid(row=0, column=2, padx=(6, 0))
        
        ttk.Label(self.tag_frame, text="(comma-separated)").grid(row=0, column=3, padx=(6, 0))
        
        # Tag search mode
        self.tag_mode_frame = ttk.Frame(inputs)
        ttk.Label(self.tag_mode_frame, text="Match mode:").grid(row=0, column=0, sticky=tk.W)
        self.tag_mode_var = tk.StringVar(value="ANY")
        self.tag_mode_combo = ttk.Combobox(self.tag_mode_frame, textvariable=self.tag_mode_var, 
                                         values=["ANY", "ALL"], state="readonly", width=8)
        self.tag_mode_combo.grid(row=0, column=1, padx=(6, 0))
        ttk.Label(self.tag_mode_frame, text="(ANY = photos with any tag, ALL = photos with all tags)").grid(row=0, column=2, padx=(6, 0))

        # Date search inputs
        self.date_frame = ttk.Frame(inputs)
        ttk.Label(self.date_frame, text="From date:").grid(row=0, column=0, sticky=tk.W)
        self.date_from_var = tk.StringVar()
        self.date_from_entry = ttk.Entry(self.date_frame, textvariable=self.date_from_var, width=12, state="readonly")
        self.date_from_entry.grid(row=0, column=1, padx=(6, 0))
        
        # Calendar button for date from
        def open_calendar_from():
            current_date = self.date_from_var.get()
            selected_date = self.gui_core.create_calendar_dialog(panel, "Select From Date", current_date)
            if selected_date is not None:
                self.date_from_var.set(selected_date)
        
        self.date_from_btn = ttk.Button(self.date_frame, text="📅", width=3, command=open_calendar_from)
        self.date_from_btn.grid(row=0, column=2, padx=(6, 0))
        ttk.Label(self.date_frame, text="(YYYY-MM-DD)").grid(row=0, column=3, padx=(6, 0))
        
        self.date_to_frame = ttk.Frame(inputs)
        ttk.Label(self.date_to_frame, text="To date:").grid(row=0, column=0, sticky=tk.W)
        self.date_to_var = tk.StringVar()
        self.date_to_entry = ttk.Entry(self.date_to_frame, textvariable=self.date_to_var, width=12, state="readonly")
        self.date_to_entry.grid(row=0, column=1, padx=(6, 0))
        
        # Calendar button for date to
        def open_calendar_to():
            current_date = self.date_to_var.get()
            selected_date = self.gui_core.create_calendar_dialog(panel, "Select To Date", current_date)
            if selected_date is not None:
                self.date_to_var.set(selected_date)
        
        self.date_to_btn = ttk.Button(self.date_to_frame, text="📅", width=3, command=open_calendar_to)
        self.date_to_btn.grid(row=0, column=2, padx=(6, 0))
        ttk.Label(self.date_to_frame, text="(YYYY-MM-DD, optional)").grid(row=0, column=3, padx=(6, 0))

        # Planned inputs (stubs)
        self.planned_label = ttk.Label(inputs, text="This search type is planned and not yet implemented.", foreground="#888")

        # Results area
        results_frame = ttk.Frame(panel)
        results_frame.grid(row=3, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
        results_frame.columnconfigure(0, weight=1)
        results_frame.rowconfigure(1, weight=1)
        
        # Results header with count
        results_header = ttk.Frame(results_frame)
        results_header.grid(row=0, column=0, sticky=(tk.W, tk.E))
        results_label = ttk.Label(results_header, text="Results:", font=("Arial", 10, "bold"))
        results_label.grid(row=0, column=0, sticky=tk.W)
        self.results_count_label = ttk.Label(results_header, text="(0 items)", font=("Arial", 10), foreground="gray")
        self.results_count_label.grid(row=0, column=1, padx=(6, 0))

        columns = ("select", "person", "tags", "processed", "open_dir", "open_photo", "path", "date_taken")
        self.tree = ttk.Treeview(results_frame, columns=columns, show="headings", selectmode="browse")
        self.tree.heading("select", text="☑")
        self.tree.heading("person", text="Person", command=lambda: self.sort_treeview("person"))
        self.tree.heading("tags", text="Tags", command=lambda: self.sort_treeview("tags"))
        self.tree.heading("processed", text="Processed", command=lambda: self.sort_treeview("processed"))
        self.tree.heading("open_dir", text="📁")
        self.tree.heading("open_photo", text="👤")
        self.tree.heading("path", text="Photo path", command=lambda: self.sort_treeview("path"))
        self.tree.heading("date_taken", text="Date Taken", command=lambda: self.sort_treeview("date_taken"))
        self.tree.column("select", width=50, anchor="center")
        self.tree.column("person", width=180, anchor="w")
        self.tree.column("tags", width=200, anchor="w")
        self.tree.column("processed", width=80, anchor="center")
        self.tree.column("open_dir", width=50, anchor="center")
        self.tree.column("open_photo", width=50, anchor="center")
        self.tree.column("path", width=400, anchor="w")
        self.tree.column("date_taken", width=100, anchor="center")
        
        # Add vertical scrollbar for the treeview
        tree_v_scrollbar = ttk.Scrollbar(results_frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=tree_v_scrollbar.set)
        
        # Pack treeview and scrollbar
        self.tree.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(4, 0))
        tree_v_scrollbar.grid(row=1, column=1, sticky=(tk.N, tk.S), pady=(4, 0))

        # Buttons
        btns = ttk.Frame(panel)
        btns.grid(row=4, column=0, sticky=(tk.W, tk.E), pady=(8, 0))
        search_btn = ttk.Button(btns, text="Search", command=lambda: self.do_search())
        search_btn.grid(row=0, column=0, sticky=tk.W)
        tag_btn = ttk.Button(btns, text="Tag selected photos", command=lambda: self.tag_selected_photos())
        tag_btn.grid(row=0, column=1, padx=(6, 0), sticky=tk.W)
        clear_btn = ttk.Button(btns, text="Clear all selected", command=lambda: self.clear_all_selected())
        clear_btn.grid(row=0, column=2, padx=(6, 0), sticky=tk.W)

        # Set up event handlers
        type_combo.bind("<<ComboboxSelected>>", self.switch_inputs)
        self.switch_inputs()
        self.tree.bind("<Button-1>", self.on_tree_click)
        self.tree.bind("<Motion>", self.on_tree_motion)
        self.tree.bind("<Leave>", self.hide_tooltip)

        # Enter key bindings
        self.name_entry.bind("<Return>", lambda e: self.do_search())
        self.tag_entry.bind("<Return>", lambda e: self.do_search())
        folder_entry.bind("<Return>", lambda e: self.do_search())

        # Initialize tooltip system
        self.tooltip = None
        
        # Set up help icon tooltip
        self._setup_help_icon_tooltip()
        
        return panel

    def _setup_help_icon_tooltip(self):
        """Set up tooltip for the help icon"""
        def show_available_tags_tooltip(event):
            # Get all available tags from database
            try:
                tag_id_to_name, tag_name_to_id = self.db.load_tag_mappings()
                available_tags = sorted(tag_name_to_id.keys())
                
                if available_tags:
                    # Create tooltip with tags in a column format
                    tag_list = "\n".join(available_tags)
                    tooltip_text = f"Available tags:\n{tag_list}"
                else:
                    tooltip_text = "No tags available in database"
                
                self.show_tooltip(self.tag_help_icon, event.x_root, event.y_root, tooltip_text)
            except Exception:
                self.show_tooltip(self.tag_help_icon, event.x_root, event.y_root, "Error loading tags")
        
        # Bind tooltip events to help icon
        self.tag_help_icon.bind("<Enter>", show_available_tags_tooltip)
        self.tag_help_icon.bind("<Leave>", self.hide_tooltip)

    def switch_inputs(self, *_):
        """Switch input fields based on search type"""
        # Clear results when search type changes
        self.clear_results()
        
        for w in self.name_frame.master.winfo_children():
            if w != self.name_frame.master:  # Don't hide the inputs frame itself
                w.grid_remove()
        
        choice = self.search_type_var.get()
        if choice == self.SEARCH_TYPES[0]:  # Search photos by name
            self.name_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
            self.name_entry.configure(state="normal")
            self.tag_entry.configure(state="disabled")
            self.tag_mode_combo.configure(state="disabled")
            self.date_from_entry.configure(state="disabled")
            self.date_to_entry.configure(state="disabled")
            self.date_from_btn.configure(state="disabled")
            self.date_to_btn.configure(state="disabled")
            # Show person column for name search
            self.tree.column("person", width=180, minwidth=50, anchor="w")
            self.tree.heading("person", text="Person", command=lambda: self.sort_treeview("person"))
            # Restore people icon column for name search
            self.tree.column("open_photo", width=50, minwidth=50, anchor="center")
            self.tree.heading("open_photo", text="👤")
            # Restore all columns to display (hide processed column for name search)
            self.tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path", "date_taken")
        elif choice == self.SEARCH_TYPES[1]:  # Search photos by date
            self.date_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
            self.date_to_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(4, 0))
            self.name_entry.configure(state="disabled")
            self.tag_entry.configure(state="disabled")
            self.tag_mode_combo.configure(state="disabled")
            self.date_from_entry.configure(state="readonly")
            self.date_to_entry.configure(state="readonly")
            self.date_from_btn.configure(state="normal")
            self.date_to_btn.configure(state="normal")
            # Hide person column for date search
            self.tree.column("person", width=0, minwidth=0, anchor="w")
            self.tree.heading("person", text="")
            # Restore people icon column for date search
            self.tree.column("open_photo", width=50, minwidth=50, anchor="center")
            self.tree.heading("open_photo", text="👤")
            # Show all columns except person for date search
            self.tree["displaycolumns"] = ("select", "tags", "processed", "open_dir", "open_photo", "path", "date_taken")
        elif choice == self.SEARCH_TYPES[2]:  # Search photos by tags
            self.tag_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
            self.tag_mode_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(4, 0))
            self.name_entry.configure(state="disabled")
            self.tag_entry.configure(state="normal")
            self.tag_mode_combo.configure(state="readonly")
            self.date_from_entry.configure(state="disabled")
            self.date_to_entry.configure(state="disabled")
            self.date_from_btn.configure(state="disabled")
            self.date_to_btn.configure(state="disabled")
            # Hide person column completely for tag search
            self.tree.column("person", width=0, minwidth=0, anchor="w")
            self.tree.heading("person", text="")
            # Restore people icon column for tag search
            self.tree.column("open_photo", width=50, minwidth=50, anchor="center")
            self.tree.heading("open_photo", text="👤")
            # Also hide the column from display
            self.tree["displaycolumns"] = ("select", "tags", "processed", "open_dir", "open_photo", "path", "date_taken")
        elif choice == self.SEARCH_TYPES[6]:  # Photos without faces
            # No input needed for this search type
            # Hide person column since photos without faces won't have person info
            self.tree.column("person", width=0, minwidth=0, anchor="w")
            self.tree.heading("person", text="")
            # Hide the people icon column since there are no faces/people
            self.tree.column("open_photo", width=0, minwidth=0, anchor="center")
            self.tree.heading("open_photo", text="")
            # Also hide the columns from display
            self.tree["displaycolumns"] = ("select", "tags", "processed", "open_dir", "path", "date_taken")
            # Auto-run search for photos without faces
            self.do_search()
        elif choice == self.SEARCH_TYPES[7]:  # Photos without tags
            # No input needed for this search type
            # Hide person column for photos without tags search
            self.tree.column("person", width=0, minwidth=0, anchor="w")
            self.tree.heading("person", text="")
            # Show the people icon column since there might be faces/people
            self.tree.column("open_photo", width=50, minwidth=50, anchor="center")
            self.tree.heading("open_photo", text="👤")
            # Show all columns except person for photos without tags search
            self.tree["displaycolumns"] = ("select", "tags", "processed", "open_dir", "open_photo", "path", "date_taken")
            # Auto-run search for photos without tags
            self.do_search()
        else:
            self.planned_label.grid(row=0, column=0, sticky=tk.W)
            self.name_entry.configure(state="disabled")
            self.tag_entry.configure(state="disabled")
            self.tag_mode_combo.configure(state="disabled")
            self.date_from_entry.configure(state="disabled")
            self.date_to_entry.configure(state="disabled")
            self.date_from_btn.configure(state="disabled")
            self.date_to_btn.configure(state="disabled")
            # Hide person column for other search types
            self.tree.column("person", width=0, minwidth=0, anchor="w")
            self.tree.heading("person", text="")
            # Restore people icon column for other search types
            self.tree.column("open_photo", width=50, minwidth=50, anchor="center")
            self.tree.heading("open_photo", text="👤")
            # Show all columns except person for other search types
            self.tree["displaycolumns"] = ("select", "tags", "processed", "open_dir", "open_photo", "path", "date_taken")

    def filter_results_by_folder(self, results, folder_path):
        """Filter search results by folder path if specified."""
        if not folder_path or not folder_path.strip():
            return results
        
        folder_path = folder_path.strip()
        filtered_results = []
        
        for result in results:
            if len(result) >= 1:
                # Extract photo path from result tuple (always at index 0)
                photo_path = result[0]
                
                # Check if photo path starts with the specified folder path
                if photo_path.startswith(folder_path):
                    filtered_results.append(result)
        
        return filtered_results

    def clear_results(self):
        """Clear all results from the treeview"""
        for i in self.tree.get_children():
            self.tree.delete(i)
        # Reset sorting state for new search
        self.sort_column = None
        self.sort_reverse = False
        # Clear selection tracking
        self.selected_photos.clear()
        # Clear tag cache
        self.photo_tags_cache.clear()
        # Reset results count
        self.results_count_label.config(text="(0 items)")
        self.update_header_display()

    def add_results(self, rows):
        """Add search results to the treeview"""
        # rows expected: List[(full_name, path)] or List[(path, tag_info)] for tag search or List[(path, date_taken)] for date search
        for row in rows:
            if len(row) == 2:
                if self.search_type_var.get() == self.SEARCH_TYPES[1]:  # Date search
                    # For date search: (path, date_taken) - hide person column
                    path, date_taken = row
                    photo_tags = self.get_photo_tags_for_display(path)
                    processed_status = self.get_photo_processed_status(path)
                    self.tree.insert("", tk.END, values=("☐", "", photo_tags, processed_status, "📁", "👤", path, date_taken))
                elif self.search_type_var.get() == self.SEARCH_TYPES[2]:  # Tag search
                    # For tag search: (path, tag_info) - hide person column
                    # Show ALL tags for the photo, not just matching ones
                    path, tag_info = row
                    photo_tags = self.get_photo_tags_for_display(path)
                    date_taken = self.get_photo_date_taken(path)
                    processed_status = self.get_photo_processed_status(path)
                    self.tree.insert("", tk.END, values=("☐", "", photo_tags, processed_status, "📁", "👤", path, date_taken))
                elif self.search_type_var.get() == self.SEARCH_TYPES[6]:  # Photos without faces
                    # For photos without faces: (path, tag_info) - hide person and people icon columns
                    path, tag_info = row
                    photo_tags = self.get_photo_tags_for_display(path)
                    date_taken = self.get_photo_date_taken(path)
                    processed_status = self.get_photo_processed_status(path)
                    self.tree.insert("", tk.END, values=("☐", "", photo_tags, processed_status, "📁", "", path, date_taken))
                elif self.search_type_var.get() == self.SEARCH_TYPES[7]:  # Photos without tags
                    # For photos without tags: (path, filename) - hide person column
                    path, filename = row
                    photo_tags = self.get_photo_tags_for_display(path)  # Will be "No tags"
                    date_taken = self.get_photo_date_taken(path)
                    processed_status = self.get_photo_processed_status(path)
                    self.tree.insert("", tk.END, values=("☐", "", photo_tags, processed_status, "📁", "👤", path, date_taken))
                else:
                    # For name search: (path, full_name) - show person column
                    p, full_name = row
                    # Get tags for this photo
                    photo_tags = self.get_photo_tags_for_display(p)
                    date_taken = self.get_photo_date_taken(p)
                    processed_status = self.get_photo_processed_status(p)
                    self.tree.insert("", tk.END, values=("☐", full_name, photo_tags, processed_status, "📁", "👤", p, date_taken))
        
        # Sort by appropriate column by default when results are first loaded
        if rows and self.sort_column is None:
            if self.search_type_var.get() == self.SEARCH_TYPES[1]:  # Date search
                # Sort by date_taken column for date search
                self.sort_column = "date_taken"
            elif self.search_type_var.get() == self.SEARCH_TYPES[2]:  # Tag search
                # Sort by tags column for tag search
                self.sort_column = "tags"
            elif self.search_type_var.get() == self.SEARCH_TYPES[6]:  # Photos without faces
                # Sort by path column for photos without faces
                self.sort_column = "path"
            elif self.search_type_var.get() == self.SEARCH_TYPES[7]:  # Photos without tags
                # Sort by path column for photos without tags (person column is hidden)
                self.sort_column = "path"
            else:
                # Sort by person column for name search
                self.sort_column = "person"
            
            self.sort_reverse = False
            # Get all items and sort them directly
            items = [(self.tree.set(child, self.sort_column), child) for child in self.tree.get_children('')]
            if self.sort_column == 'date_taken':
                # Sort by date, handling "No date" entries
                def date_sort_key(item):
                    date_str = item[0]
                    if date_str == "No date":
                        return "9999-12-31"  # Put "No date" entries at the end
                    return date_str
                items.sort(key=date_sort_key, reverse=False)  # Ascending
            else:
                items.sort(key=lambda x: x[0].lower(), reverse=False)  # Ascending
            # Reorder items in treeview
            for index, (val, child) in enumerate(items):
                self.tree.move(child, '', index)
            # Update header display
            self.update_header_display()
        
        # Update results count
        item_count = len(self.tree.get_children())
        self.results_count_label.config(text=f"({item_count} items)")

    def do_search(self):
        """Perform the search based on current search type and parameters"""
        self.clear_results()
        choice = self.search_type_var.get()
        folder_filter = self.folder_var.get().strip()
        
        if choice == self.SEARCH_TYPES[0]:  # Search photos by name
            query = self.name_var.get().strip()
            if not query:
                messagebox.showinfo("Search", "Please enter a name to search.", parent=self.parent_frame)
                return
            rows = self.search_stats.search_faces(query)
            # Apply folder filter
            rows = self.filter_results_by_folder(rows, folder_filter)
            if not rows:
                folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
                messagebox.showinfo("Search", f"No photos found for '{query}'{folder_msg}.", parent=self.parent_frame)
            self.add_results(rows)
        elif choice == self.SEARCH_TYPES[1]:  # Search photos by date
            date_from = self.date_from_var.get().strip()
            date_to = self.date_to_var.get().strip()
            
            # Validate date format if provided
            if date_from:
                try:
                    from datetime import datetime
                    datetime.strptime(date_from, '%Y-%m-%d')
                except ValueError:
                    messagebox.showerror("Invalid Date", "From date must be in YYYY-MM-DD format.", parent=self.parent_frame)
                    return
            
            if date_to:
                try:
                    from datetime import datetime
                    datetime.strptime(date_to, '%Y-%m-%d')
                except ValueError:
                    messagebox.showerror("Invalid Date", "To date must be in YYYY-MM-DD format.", parent=self.parent_frame)
                    return
            
            # Check if at least one date is provided
            if not date_from and not date_to:
                messagebox.showinfo("Search", "Please enter at least one date (from date or to date).", parent=self.parent_frame)
                return
            
            rows = self.search_stats.search_photos_by_date(date_from if date_from else None, 
                                                         date_to if date_to else None)
            # Apply folder filter
            rows = self.filter_results_by_folder(rows, folder_filter)
            if not rows:
                date_range_text = ""
                if date_from and date_to:
                    date_range_text = f" between {date_from} and {date_to}"
                elif date_from:
                    date_range_text = f" from {date_from}"
                elif date_to:
                    date_range_text = f" up to {date_to}"
                folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
                messagebox.showinfo("Search", f"No photos found{date_range_text}{folder_msg}.", parent=self.parent_frame)
            else:
                # Convert to the format expected by add_results: (path, date_taken)
                formatted_rows = [(path, date_taken) for path, date_taken in rows]
                self.add_results(formatted_rows)
        elif choice == self.SEARCH_TYPES[2]:  # Search photos by tags
            tag_query = self.tag_var.get().strip()
            if not tag_query:
                messagebox.showinfo("Search", "Please enter tags to search for.", parent=self.parent_frame)
                return
            
            # Parse comma-separated tags
            tags = [tag.strip() for tag in tag_query.split(',') if tag.strip()]
            if not tags:
                messagebox.showinfo("Search", "Please enter valid tags to search for.", parent=self.parent_frame)
                return
            
            # Determine match mode
            match_all = (self.tag_mode_var.get() == "ALL")
            
            rows = self.search_stats.search_photos_by_tags(tags, match_all)
            # Apply folder filter
            rows = self.filter_results_by_folder(rows, folder_filter)
            if not rows:
                mode_text = "all" if match_all else "any"
                folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
                messagebox.showinfo("Search", f"No photos found with {mode_text} of the tags: {', '.join(tags)}{folder_msg}", parent=self.parent_frame)
            self.add_results(rows)
        elif choice == self.SEARCH_TYPES[6]:  # Photos without faces
            rows = self.search_stats.get_photos_without_faces()
            # Apply folder filter
            rows = self.filter_results_by_folder(rows, folder_filter)
            if not rows:
                folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
                messagebox.showinfo("Search", f"No photos without faces found{folder_msg}.", parent=self.parent_frame)
            else:
                # Convert to the format expected by add_results: (path, tag_info)
                # For photos without faces, we don't have person info, so we use empty string
                formatted_rows = [(path, "") for path, filename in rows]
                self.add_results(formatted_rows)
        elif choice == self.SEARCH_TYPES[7]:  # Photos without tags
            rows = self.search_stats.get_photos_without_tags()
            # Apply folder filter
            rows = self.filter_results_by_folder(rows, folder_filter)
            if not rows:
                folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
                messagebox.showinfo("Search", f"No photos without tags found{folder_msg}.", parent=self.parent_frame)
            else:
                # Convert to the format expected by add_results: (path, filename)
                # For photos without tags, we have both path and filename
                formatted_rows = [(path, filename) for path, filename in rows]
                self.add_results(formatted_rows)

    def sort_treeview(self, col: str):
        """Sort the treeview by the specified column."""
        # Get all items and their values
        items = [(self.tree.set(child, col), child) for child in self.tree.get_children('')]
        
        # Determine sort direction
        if self.sort_column == col:
            # Same column clicked - toggle direction
            self.sort_reverse = not self.sort_reverse
        else:
            # Different column clicked - start with ascending
            self.sort_reverse = False
            self.sort_column = col
        
        # Sort the items
        # For person, tags, and path columns, sort alphabetically
        # For date_taken column, sort by date
        # For processed column, sort by processed status (Yes/No)
        # For icon columns, maintain original order
        if col in ['person', 'tags', 'path']:
            items.sort(key=lambda x: x[0].lower(), reverse=self.sort_reverse)
        elif col == 'date_taken':
            # Sort by date, handling "No date" entries
            def date_sort_key(item):
                date_str = item[0]
                if date_str == "No date":
                    return "9999-12-31"  # Put "No date" entries at the end
                return date_str
            items.sort(key=date_sort_key, reverse=self.sort_reverse)
        elif col == 'processed':
            # Sort by processed status (Yes comes before No)
            def processed_sort_key(item):
                processed_str = item[0]
                if processed_str == "Yes":
                    return "0"  # Yes comes first
                else:
                    return "1"  # No comes second
            items.sort(key=processed_sort_key, reverse=self.sort_reverse)
        else:
            # For icon columns, just reverse if clicking same column
            if self.sort_column == col and self.sort_reverse:
                items.reverse()
        
        # Reorder items in treeview
        for index, (val, child) in enumerate(items):
            self.tree.move(child, '', index)
        
        # Update header display
        self.update_header_display()
    
    def update_header_display(self):
        """Update header display to show sort indicators."""
        # Reset all headers
        self.tree.heading("person", text="Person")
        self.tree.heading("tags", text="Tags")
        self.tree.heading("processed", text="Processed")
        self.tree.heading("path", text="Photo path")
        self.tree.heading("date_taken", text="Date Taken")
        
        # Add sort indicator to current sort column
        if self.sort_column == "person":
            indicator = " ↓" if self.sort_reverse else " ↑"
            self.tree.heading("person", text="Person" + indicator)
        elif self.sort_column == "tags":
            indicator = " ↓" if self.sort_reverse else " ↑"
            self.tree.heading("tags", text="Tags" + indicator)
        elif self.sort_column == "processed":
            indicator = " ↓" if self.sort_reverse else " ↑"
            self.tree.heading("processed", text="Processed" + indicator)
        elif self.sort_column == "path":
            indicator = " ↓" if self.sort_reverse else " ↑"
            self.tree.heading("path", text="Photo path" + indicator)
        elif self.sort_column == "date_taken":
            indicator = " ↓" if self.sort_reverse else " ↑"
            self.tree.heading("date_taken", text="Date Taken" + indicator)

    def on_tree_click(self, event):
        """Handle clicks on the treeview"""
        region = self.tree.identify("region", event.x, event.y)
        if region != "cell":
            return
        row_id = self.tree.identify_row(event.y)
        col_id = self.tree.identify_column(event.x)  # '#1', '#2', ...
        if not row_id or not col_id:
            return
        vals = self.tree.item(row_id, "values")
        if not vals or len(vals) < 6:
            return
        
        # Determine column offsets based on search type
        is_name_search = (self.search_type_var.get() == self.SEARCH_TYPES[0])
        is_photos_without_faces = (self.search_type_var.get() == self.SEARCH_TYPES[6])
        
        if is_name_search:
            # Name search: all columns visible including person (processed column hidden)
            select_col = "#1"      # select is column 1
            open_dir_col = "#4"    # open_dir is column 4
            face_col = "#5"        # open_photo is column 5
            path_col = "#6"        # path is column 6
            path_index = 6         # path is at index 6 in values array (still same since processed is hidden from display)
        elif is_photos_without_faces:
            # Photos without faces: person and people icon columns are hidden
            select_col = "#1"      # select is column 1
            open_dir_col = "#4"    # open_dir is column 4
            face_col = "#5"        # open_photo is column 5 (but hidden)
            path_col = "#5"        # path is column 5 (since people icon is hidden)
            path_index = 6         # path is at index 6 in values array
        else:
            # All other searches: person column is hidden, people icon visible
            select_col = "#1"      # select is column 1
            open_dir_col = "#4"    # open_dir is column 4
            face_col = "#5"        # open_photo is column 5
            path_col = "#6"        # path is column 6
            path_index = 6         # path is at index 6 in values array
        
        path = vals[path_index]  # Photo path
        if col_id == open_dir_col:  # Open directory column
            self.open_dir(path)
        elif col_id == face_col:  # Face icon column
            # No popup needed, just tooltip
            pass
        elif col_id == path_col:  # Photo path column - clickable to open photo
            try:
                import os
                import sys
                if os.name == "nt":
                    os.startfile(path)  # type: ignore[attr-defined]
                elif sys.platform == "darwin":
                    import subprocess
                    subprocess.run(["open", path], check=False)
                else:
                    import subprocess
                    subprocess.run(["xdg-open", path], check=False)
            except Exception:
                messagebox.showerror("Open Photo", "Failed to open the selected photo.", parent=self.parent_frame)
        elif col_id == select_col:  # Checkbox column
            self.toggle_photo_selection(row_id, vals)

    def on_tree_motion(self, event):
        """Handle mouse motion over the treeview for tooltips"""
        region = self.tree.identify("region", event.x, event.y)
        if region != "cell":
            self.hide_tooltip()
            self.tree.config(cursor="")
            return
        col_id = self.tree.identify_column(event.x)
        row_id = self.tree.identify_row(event.y)
        
        # Determine column offsets based on search type
        is_name_search = (self.search_type_var.get() == self.SEARCH_TYPES[0])
        is_photos_without_faces = (self.search_type_var.get() == self.SEARCH_TYPES[6])
        
        if is_name_search:
            # Name search: all columns visible including person (processed column hidden)
            tags_col = "#3"        # tags is column 3
            open_dir_col = "#4"    # open_dir is column 4
            face_col = "#5"        # open_photo is column 5
            path_col = "#6"        # path is column 6
            path_index = 6         # path is at index 6 in values array (still same since processed is hidden from display)
        elif is_photos_without_faces:
            # Photos without faces: person and people icon columns are hidden
            tags_col = "#2"        # tags is column 2
            open_dir_col = "#4"    # open_dir is column 4
            face_col = "#5"        # open_photo is column 5 (but hidden)
            path_col = "#5"        # path is column 5 (since people icon is hidden)
            path_index = 6         # path is at index 6 in values array
        else:
            # All other searches: person column is hidden, people icon visible
            tags_col = "#2"        # tags is column 2
            open_dir_col = "#4"    # open_dir is column 4
            face_col = "#5"        # open_photo is column 5
            path_col = "#6"        # path is column 6
            path_index = 6         # path is at index 6 in values array
        
        if col_id == tags_col:  # Tags column
            self.tree.config(cursor="")
            # Show tags tooltip
            if row_id:
                vals = self.tree.item(row_id, "values")
                if len(vals) >= 3:
                    # Tags are at index 2 for all search types (after select, person is hidden in most)
                    tags_text = vals[2]
                    self.show_tooltip(self.tree, event.x_root, event.y_root, f"Tags: {tags_text}")
        elif col_id == open_dir_col:  # Open directory column
            self.tree.config(cursor="hand2")
            self.show_tooltip(self.tree, event.x_root, event.y_root, "Open file location")
        elif col_id == face_col:  # Face icon column
            self.tree.config(cursor="hand2")
            # Show people tooltip
            if row_id:
                vals = self.tree.item(row_id, "values")
                if len(vals) >= 5:
                    path = vals[path_index]
                    people_text = self.get_photo_people_tooltip(path)
                    self.show_tooltip(self.tree, event.x_root, event.y_root, people_text)
        elif col_id == path_col:  # Photo path column
            self.tree.config(cursor="hand2")
            self.show_tooltip(self.tree, event.x_root, event.y_root, "Open photo")
        else:
            self.tree.config(cursor="")
            self.hide_tooltip()

    def show_tooltip(self, widget, x, y, text: str):
        """Show a tooltip"""
        self.hide_tooltip()
        try:
            self.tooltip = tk.Toplevel(widget)
            self.tooltip.wm_overrideredirect(True)
            self.tooltip.wm_geometry(f"+{x+12}+{y+12}")
            lbl = tk.Label(self.tooltip, text=text, background="lightyellow", relief="solid", borderwidth=1, font=("Arial", 9))
            lbl.pack()
        except Exception:
            self.tooltip = None

    def hide_tooltip(self, *_):
        """Hide the current tooltip"""
        if self.tooltip is not None:
            try:
                self.tooltip.destroy()
            except Exception:
                pass
            self.tooltip = None

    def open_dir(self, path: str):
        """Open the directory containing the photo"""
        try:
            import os
            import sys
            folder = os.path.dirname(path)
            if os.name == "nt":
                os.startfile(folder)  # type: ignore[attr-defined]
            elif sys.platform == "darwin":
                import subprocess
                subprocess.run(["open", folder], check=False)
            else:
                import subprocess
                subprocess.run(["xdg-open", folder], check=False)
        except Exception:
            messagebox.showerror("Open Location", "Failed to open the file location.", parent=self.parent_frame)

    def toggle_photo_selection(self, row_id, vals):
        """Toggle checkbox selection for a photo."""
        if len(vals) < 7:
            return
        current_state = vals[0]  # Checkbox is now in column 0 (first)
        path = vals[6]  # Photo path is now in column 6 (last)
        if current_state == "☐":
            # Select photo
            new_state = "☑"
            self.selected_photos[path] = {
                'person': vals[1],  # Person is now in column 1
                'path': path
            }
        else:
            # Deselect photo
            new_state = "☐"
            if path in self.selected_photos:
                del self.selected_photos[path]
        
        # Update the treeview
        new_vals = list(vals)
        new_vals[0] = new_state
        self.tree.item(row_id, values=new_vals)

    def tag_selected_photos(self):
        """Open linkage dialog for selected photos."""
        if not self.selected_photos:
            messagebox.showinfo("Tag Photos", "Please select photos to tag first.", parent=self.parent_frame)
            return
        
        # Get photo IDs for selected photos
        selected_photo_ids = []
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            for path in self.selected_photos.keys():
                cursor.execute('SELECT id FROM photos WHERE path = ?', (path,))
                result = cursor.fetchone()
                if result:
                    selected_photo_ids.append(result[0])
        
        if not selected_photo_ids:
            messagebox.showerror("Tag Photos", "Could not find photo IDs for selected photos.", parent=self.parent_frame)
            return
        
        # Open the linkage dialog
        self.open_linkage_dialog(selected_photo_ids)

    def clear_all_selected(self):
        """Clear all selected photos and update checkboxes."""
        if not self.selected_photos:
            return
        
        # Clear the selection tracking
        self.selected_photos.clear()
        
        # Update all checkboxes to unselected state
        for item in self.tree.get_children():
            vals = self.tree.item(item, "values")
            if len(vals) >= 7 and vals[0] == "☑":
                new_vals = list(vals)
                new_vals[0] = "☐"
                self.tree.item(item, values=new_vals)

    def get_photo_tags_for_display(self, photo_path):
        """Get tags for a photo to display in the tags column."""
        # Check cache first
        if photo_path in self.photo_tags_cache:
            tag_names = self.photo_tags_cache[photo_path]
        else:
            # Load from database and cache
            try:
                with self.db.get_db_connection() as conn:
                    cursor = conn.cursor()
                    cursor.execute('SELECT id FROM photos WHERE path = ?', (photo_path,))
                    result = cursor.fetchone()
                    if not result:
                        return "No photo found"
                    
                    photo_id = result[0]
                    cursor.execute('''
                        SELECT t.tag_name 
                        FROM tags t 
                        JOIN phototaglinkage ptl ON t.id = ptl.tag_id 
                        WHERE ptl.photo_id = ?
                        ORDER BY t.tag_name
                    ''', (photo_id,))
                    tag_names = [row[0] for row in cursor.fetchall()]
                    self.photo_tags_cache[photo_path] = tag_names
            except Exception:
                return "No tags"
        
        # Format for display - show all tags
        if tag_names:
            return ', '.join(tag_names)
        else:
            return "No tags"

    def get_photo_date_taken(self, photo_path):
        """Get date_taken for a photo to display in the date_taken column."""
        try:
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('SELECT date_taken FROM photos WHERE path = ?', (photo_path,))
                result = cursor.fetchone()
                if result and result[0]:
                    return result[0]  # Return the date as stored in database
                else:
                    return "No date"  # No date_taken available
        except Exception:
            return "No date"

    def get_photo_processed_status(self, photo_path):
        """Get processed status for a photo to display in the processed column."""
        try:
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('SELECT processed FROM photos WHERE path = ?', (photo_path,))
                result = cursor.fetchone()
                if result and result[0] is not None:
                    return "Yes" if result[0] else "No"
                else:
                    return "No"  # Default to not processed
        except Exception:
            return "No"

    def get_photo_people_tooltip(self, photo_path):
        """Get people information for a photo to display in tooltip."""
        try:
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    SELECT DISTINCT pe.first_name, pe.last_name, pe.middle_name, pe.maiden_name
                    FROM photos p
                    JOIN faces f ON p.id = f.photo_id
                    JOIN people pe ON f.person_id = pe.id
                    WHERE p.path = ? AND f.person_id IS NOT NULL
                    ORDER BY pe.last_name, pe.first_name
                ''', (photo_path,))
                people = cursor.fetchall()
                
                if not people:
                    return "No people identified"
                
                people_names = []
                for person in people:
                    first = (person[0] or "").strip()
                    last = (person[1] or "").strip()
                    middle = (person[2] or "").strip()
                    maiden = (person[3] or "").strip()
                    
                    # Build full name
                    name_parts = []
                    if first:
                        name_parts.append(first)
                    if middle:
                        name_parts.append(middle)
                    if last:
                        name_parts.append(last)
                    if maiden and maiden != last:
                        name_parts.append(f"({maiden})")
                    
                    full_name = " ".join(name_parts) if name_parts else "Unknown"
                    people_names.append(full_name)
                
                if people_names:
                    if len(people_names) <= 3:
                        return f"People: {', '.join(people_names)}"
                    else:
                        return f"People: {', '.join(people_names[:3])}... (+{len(people_names)-3} more)"
                else:
                    return "No people identified"
        except Exception:
            pass
        return "No people identified"

    def open_linkage_dialog(self, photo_ids):
        """Open the linkage dialog for selected photos using tag manager functionality."""
        popup = tk.Toplevel(self.parent_frame)
        popup.title("Tag Selected Photos")
        popup.transient(self.parent_frame)
        popup.grab_set()
        popup.geometry("500x400")
        popup.resizable(True, True)
        
        # Track tag changes for updating results
        tags_added = set()  # tag names that were added
        tags_removed = set()  # tag names that were removed

        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)

        ttk.Label(top_frame, text=f"Selected Photos: {len(photo_ids)}").grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=(0,6))
        ttk.Label(top_frame, text="Add tag:").grid(row=1, column=0, padx=(0, 8), sticky=tk.W)

        # Get existing tags using tag manager
        tag_id_to_name, tag_name_to_id = self.db.load_tag_mappings()
        existing_tags = sorted(tag_name_to_id.keys())

        tag_var = tk.StringVar()
        combo = ttk.Combobox(top_frame, textvariable=tag_var, values=existing_tags, width=30)
        combo.grid(row=1, 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
            
            # Resolve or create tag id (case-insensitive)
            normalized_tag_name = tag_name.lower().strip()
            if normalized_tag_name in tag_name_to_id:
                tag_id = tag_name_to_id[normalized_tag_name]
            else:
                # Create new tag in database using the database method
                tag_id = self.db.add_tag(tag_name)
                if tag_id:
                    # Update mappings
                    tag_name_to_id[normalized_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()
                        # Update the combobox values to include the new tag
                        combo['values'] = existing_tags
                else:
                    messagebox.showerror("Error", f"Failed to create tag '{tag_name}'", parent=popup)
                    return

            # Add tag to all selected photos with single linkage type (0)
            affected = 0
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                for photo_id in photo_ids:
                    # Check if tag already exists for this photo
                    cursor.execute('SELECT linkage_id FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?', (photo_id, tag_id))
                    if not cursor.fetchone():
                        # Add the tag with single linkage type (0)
                        cursor.execute('INSERT INTO phototaglinkage (photo_id, tag_id, linkage_type) VALUES (?, ?, 0)', (photo_id, tag_id))
                        affected += 1

            # Track that this tag was added
            if affected > 0:
                tags_added.add(tag_name)

            # Refresh the tag list to show the new tag
            refresh_tag_list()
            tag_var.set("")

        ttk.Button(top_frame, text="Add", command=add_selected_tag).grid(row=1, column=2, padx=(8, 0))
        
        # Allow Enter key to add tag
        combo.bind('<Return>', lambda e: add_selected_tag())

        # Create scrollable tag list
        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")

        selected_tag_vars = {}

        def refresh_tag_list():
            for widget in scrollable_frame.winfo_children():
                widget.destroy()
            selected_tag_vars.clear()
            
            # Get tags that exist in ALL selected photos
            # First, get all tags for each photo
            photo_tags = {}  # photo_id -> set of tag_ids
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                for photo_id in photo_ids:
                    photo_tags[photo_id] = set()
                    cursor.execute('SELECT tag_id, linkage_type FROM phototaglinkage WHERE photo_id = ?', (photo_id,))
                    for row in cursor.fetchall():
                        photo_tags[photo_id].add(row[0])
            
            # Find intersection - tags that exist in ALL selected photos
            if not photo_tags:
                ttk.Label(scrollable_frame, text="No tags linked to selected photos", foreground="gray").pack(anchor=tk.W, pady=5)
                return
            
            # Start with tags from first photo, then intersect with others
            common_tag_ids = set(photo_tags[photo_ids[0]])
            for photo_id in photo_ids[1:]:
                common_tag_ids = common_tag_ids.intersection(photo_tags[photo_id])
            
            if not common_tag_ids:
                ttk.Label(scrollable_frame, text="No common tags found across all selected photos", foreground="gray").pack(anchor=tk.W, pady=5)
                return
            
            # Get linkage type information for common tags
            # For tags that exist in all photos, we need to determine the linkage type
            # If a tag has different linkage types across photos, we'll show the most restrictive
            common_tag_data = {}  # tag_id -> {linkage_type, photo_count}
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                for photo_id in photo_ids:
                    cursor.execute('SELECT tag_id, linkage_type FROM phototaglinkage WHERE photo_id = ? AND tag_id IN ({})'.format(','.join('?' * len(common_tag_ids))), [photo_id] + list(common_tag_ids))
                    for row in cursor.fetchall():
                        tag_id = row[0]
                        linkage_type = int(row[1]) if row[1] is not None else 0
                        if tag_id not in common_tag_data:
                            common_tag_data[tag_id] = {'linkage_type': linkage_type, 'photo_count': 0}
                        common_tag_data[tag_id]['photo_count'] += 1
                        # If we find a bulk linkage type (1), use that as it's more restrictive
                        if linkage_type == 1:
                            common_tag_data[tag_id]['linkage_type'] = 1
            
            # Sort tags by name for consistent display
            for tag_id in sorted(common_tag_data.keys()):
                tag_name = tag_id_to_name.get(tag_id, f"Unknown {tag_id}")
                var = tk.BooleanVar()
                selected_tag_vars[tag_name] = var
                frame = ttk.Frame(scrollable_frame)
                frame.pack(fill=tk.X, pady=1)
                
                # Determine if this tag can be selected for deletion
                # In single linkage dialog, only allow deleting single linkage type (0) tags
                linkage_type = common_tag_data[tag_id]['linkage_type']
                can_select = (linkage_type == 0)  # Only single linkage type can be deleted
                
                cb = ttk.Checkbutton(frame, variable=var)
                if not can_select:
                    try:
                        cb.state(["disabled"])  # disable selection for bulk tags
                    except Exception:
                        pass
                cb.pack(side=tk.LEFT, padx=(0, 5))
                
                # Display tag name with status information
                type_label = 'single' if linkage_type == 0 else 'bulk'
                photo_count = common_tag_data[tag_id]['photo_count']
                status_text = f" (saved {type_label})"
                status_color = "black" if can_select else "gray"
                ttk.Label(frame, text=tag_name + status_text, foreground=status_color).pack(side=tk.LEFT)

        def remove_selected_tags():
            tag_ids_to_remove = []
            tag_names_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])
                    tag_names_to_remove.append(tag_name)
            
            if not tag_ids_to_remove:
                return
            
            # Only remove single linkage type tags (bulk tags should be disabled anyway)
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                for photo_id in photo_ids:
                    for tag_id in tag_ids_to_remove:
                        # Double-check that this is a single linkage type before deleting
                        cursor.execute('SELECT linkage_type FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?', (photo_id, tag_id))
                        result = cursor.fetchone()
                        if result and int(result[0]) == 0:  # Only delete single linkage type
                            cursor.execute('DELETE FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?', (photo_id, tag_id))
            
            # Track that these tags were removed
            tags_removed.update(tag_names_to_remove)
            
            refresh_tag_list()

        def update_search_results():
            """Update the search results to reflect tag changes without database access."""
            if not tags_added and not tags_removed:
                return  # No changes to apply
            
            # Get photo paths for the affected photos from selected_photos
            affected_photo_paths = set(self.selected_photos.keys())
            
            # Update cache for affected photos
            for photo_path in affected_photo_paths:
                if photo_path in self.photo_tags_cache:
                    # Update cached tags based on changes
                    current_tags = set(self.photo_tags_cache[photo_path])
                    # Add new tags
                    current_tags.update(tags_added)
                    # Remove deleted tags
                    current_tags.difference_update(tags_removed)
                    # Update cache with sorted list
                    self.photo_tags_cache[photo_path] = sorted(list(current_tags))
            
            # Update each affected row in the search results
            for item in self.tree.get_children():
                vals = self.tree.item(item, "values")
                if len(vals) >= 7:
                    photo_path = vals[6]  # Photo path is at index 6
                    if photo_path in affected_photo_paths:
                        # Get current tags for this photo from cache
                        current_tags = self.get_photo_tags_for_display(photo_path)
                        # Update the tags column (index 2)
                        new_vals = list(vals)
                        new_vals[2] = current_tags
                        self.tree.item(item, values=new_vals)
        
        def close_dialog():
            """Close dialog and update search results if needed."""
            update_search_results()
            popup.destroy()

        ttk.Button(bottom_frame, text="Remove selected tags", command=remove_selected_tags).pack(side=tk.LEFT, padx=(0, 8))
        ttk.Button(bottom_frame, text="Close", command=close_dialog).pack(side=tk.RIGHT)
        refresh_tag_list()


class DashboardGUI:
    """Unified Dashboard with menu bar and content area for all features.
    
    Designed to be web-migration friendly with clear separation between
    navigation (menu bar) and content (panels).
    """
    
    def __init__(self, gui_core: GUICore, db_manager=None, face_processor=None, on_scan=None, on_process=None, on_identify=None, search_stats=None, tag_manager=None):
        self.gui_core = gui_core
        self.db_manager = db_manager
        self.face_processor = face_processor
        self.on_scan = on_scan
        self.on_process = on_process
        self.on_identify = on_identify
        self.search_stats = search_stats
        self.tag_manager = tag_manager
        
        # Panel management for future web migration
        self.panels: Dict[str, ttk.Frame] = {}
        self.current_panel: Optional[str] = None
        self.root: Optional[tk.Tk] = None

    def open(self) -> int:
        """Open the unified dashboard with menu bar and content area"""
        self.root = tk.Tk()
        self.root.title("PunimTag - Unified Dashboard")
        self.root.resizable(True, True)
        self.root.withdraw()
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        panel.columnconfigure(1, weight=2)
        panel.rowconfigure(1, weight=1)
        
        # Left panel: People list
        people_frame = ttk.LabelFrame(panel, 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)
        self.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=self.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, command=self.apply_last_name_filter)
        search_btn.pack(side=tk.LEFT, padx=(0, 5))
        clear_btn = ttk.Button(buttons_row, text="Clear", width=6, command=self.clear_last_name_filter)
        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 list with scrollbar
        people_canvas = tk.Canvas(people_frame, bg='white')
        people_scrollbar = ttk.Scrollbar(people_frame, orient="vertical", command=people_canvas.yview)
        self.people_list_inner = ttk.Frame(people_canvas)
        people_canvas.create_window((0, 0), window=self.people_list_inner, anchor="nw")
        people_canvas.configure(yscrollcommand=people_scrollbar.set)

        self.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(panel, 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 configuration
        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
        self.faces_canvas = tk.Canvas(faces_frame, bg=canvas_bg_color, highlightthickness=0)
        faces_scrollbar = ttk.Scrollbar(faces_frame, orient="vertical", command=self.faces_canvas.yview)
        self.faces_inner = ttk.Frame(self.faces_canvas)
        self.faces_canvas.create_window((0, 0), window=self.faces_inner, anchor="nw")
        self.faces_canvas.configure(yscrollcommand=faces_scrollbar.set)

        self.faces_inner.bind(
            "<Configure>",
            lambda e: self.faces_canvas.configure(scrollregion=self.faces_canvas.bbox("all"))
        )

        # Bind resize handler for responsive face grid
        self.faces_canvas.bind("<Configure>", self.on_faces_canvas_resize)

        self.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))

        # Control buttons
        control_frame = ttk.Frame(panel)
        control_frame.grid(row=2, column=0, columnspan=2, pady=(10, 0), sticky=tk.E)

        quit_btn = ttk.Button(control_frame, text="❌ Quit", command=self.on_quit)
        quit_btn.pack(side=tk.RIGHT)
        save_btn_bottom = ttk.Button(control_frame, text="💾 Save changes", command=self.on_save_all_changes)
        save_btn_bottom.pack(side=tk.RIGHT, padx=(0, 10))

        # Bind Enter key for search
        search_entry.bind('<Return>', lambda e: self.apply_last_name_filter())

        # Initial load
        self.load_people()
        self.populate_people_list()

        # Show first person's faces by default and mark selected
        if self.people_data:
            self.selected_person_id = self.people_data[0]['id']
            self.show_person_faces(self.people_data[0]['id'], self.people_data[0]['name'])

        return panel

    def on_faces_canvas_resize(self, event):
        """Handle canvas resize for responsive face grid"""
        if self.current_person_id is None:
            return
        # Debounce re-render on resize
        try:
            if self.resize_job is not None:
                self.parent_frame.after_cancel(self.resize_job)
        except Exception:
            pass
        self.resize_job = self.parent_frame.after(150, lambda: self.show_person_faces(self.current_person_id, self.current_person_name))

    def load_people(self):
        """Load people from database with counts"""
        with self.db.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
                """
            )
            self.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}"

                self.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:
            self.apply_last_name_filter()
        except Exception:
            pass

    def apply_last_name_filter(self):
        """Apply last name filter to people list"""
        query = self.last_name_search_var.get().strip().lower()
        if query:
            self.people_filtered = [p for p in self.people_data if p.get('last_name', '').lower().find(query) != -1]
        else:
            self.people_filtered = None
        self.populate_people_list()
        # Update right panel based on filtered results
        source = self.people_filtered if self.people_filtered is not None else self.people_data
        if source:
            # Load faces for the first person in the list
            first = source[0]
            try:
                # Update selection state
                for child in self.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 = self.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
            self.show_person_faces(first['id'], first.get('full_name') or first.get('name') or "")
        else:
            # No matches: clear faces panel
            self.clear_faces_panel()

    def clear_last_name_filter(self):
        """Clear last name filter"""
        self.last_name_search_var.set("")
        self.people_filtered = None
        self.populate_people_list()
        # After clearing, load faces for the first available person if any
        if self.people_data:
            first = self.people_data[0]
            try:
                for child in self.people_list_inner.winfo_children():
                    for widget in child.winfo_children():
                        if isinstance(widget, ttk.Label):
                            widget.config(font=("Arial", 10))
                first_row = self.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
            self.show_person_faces(first['id'], first.get('full_name') or first.get('name') or "")
        else:
            self.clear_faces_panel()

    def clear_faces_panel(self):
        """Clear the faces panel and cleanup resources"""
        for w in self.faces_inner.winfo_children():
            w.destroy()
        # Cleanup temp crops
        for crop in list(self.temp_crops):
            try:
                if os.path.exists(crop):
                    os.remove(crop)
            except Exception:
                pass
        self.temp_crops.clear()
        self.right_panel_images.clear()

    def unmatch_face(self, face_id: int):
        """Temporarily unmatch a face from the current person"""
        self.unmatched_faces.add(face_id)
        # Track per-person for Undo
        person_set = self.unmatched_by_person.get(self.current_person_id)
        if person_set is None:
            person_set = set()
            self.unmatched_by_person[self.current_person_id] = person_set
        person_set.add(face_id)
        # Refresh the display
        self.show_person_faces(self.current_person_id, self.current_person_name)

    def undo_changes(self):
        """Undo all temporary changes for current person"""
        if self.current_person_id in self.unmatched_by_person:
            for fid in list(self.unmatched_by_person[self.current_person_id]):
                self.unmatched_faces.discard(fid)
            self.unmatched_by_person[self.current_person_id].clear()
        # Refresh the display
        self.show_person_faces(self.current_person_id, self.current_person_name)

    def show_person_faces(self, person_id: int, person_name: str):
        """Show faces for the selected person"""
        self.current_person_id = person_id
        self.current_person_name = person_name
        self.clear_faces_panel()

        # Determine how many columns fit the available width
        available_width = self.faces_canvas.winfo_width()
        if available_width <= 1:
            available_width = 400  # Default width if canvas not yet rendered
        tile_width = 150  # approx tile + padding
        cols = max(1, available_width // tile_width)

        # Header row
        header = ttk.Label(self.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(self.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(self.unmatched_by_person.get(self.current_person_id))
        undo_btn = ttk.Button(button_frame, text="↶ Undo changes",
                              command=lambda: self.undo_changes(),
                              state="disabled" if not current_has_unmatched else "normal")
        undo_btn.pack(side=tk.LEFT, padx=(0, 10))

        # Query faces for this person
        with self.db.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 self.unmatched_faces]

        if not visible_rows:
            ttk.Label(self.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.face_processor._extract_face_crop(photo_path, location, face_id)
            thumb = None
            if crop_path and os.path.exists(crop_path):
                try:
                    from PIL import Image, ImageTk
                    img = Image.open(crop_path)
                    img.thumbnail((130, 130), Image.Resampling.LANCZOS)
                    photo_img = ImageTk.PhotoImage(img)
                    self.temp_crops.append(crop_path)
                    self.right_panel_images.append(photo_img)
                    thumb = photo_img
                except Exception:
                    thumb = None

            tile = ttk.Frame(self.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)

            style = ttk.Style()
            canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
            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: self.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(self):
        """Populate the people list with current data"""
        for w in self.people_list_inner.winfo_children():
            w.destroy()
        source = self.people_filtered if self.people_filtered is not None else self.people_data
        if not source:
            empty_label = ttk.Label(self.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(self.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):
                    # Reset all labels to normal font
                    for child in self.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"))
                    self.selected_person_id = p_id
                    # Show faces for this person
                    self.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)

                # 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: self.open_calendar(dob_var))
                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 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 self.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.db.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']
                    self.populate_people_list()
                    # Reselect and refresh right panel header if needed
                    if self.selected_person_id == current_selected_id or self.selected_person_id is None:
                        # Find updated name
                        updated = next((p for p in self.people_data if p['id'] == current_selected_id), None)
                        if updated:
                            # Bold corresponding label
                            for child in self.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
                            self.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))
                # 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 (self.selected_person_id is None and i == 0) or (self.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)

    def open_calendar(self, dob_var):
        """Open a visual calendar dialog to select date of birth"""
        from datetime import datetime, date
        import calendar

        # Create calendar window
        calendar_window = tk.Toplevel(self.parent_frame)
        calendar_window.title("Select Date of Birth")
        calendar_window.resizable(False, False)
        calendar_window.transient(self.parent_frame)
        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 on_quit(self):
        """Handle quit with unsaved changes warning"""
        # Warn if there are pending unmatched faces (unsaved changes)
        try:
            if self.unmatched_faces:
                result = messagebox.askyesnocancel(
                    "Unsaved Changes",
                    "You have pending changes that are not saved.\n\n"
                    "Yes: Save and quit\n"
                    "No: Quit without saving\n"
                    "Cancel: Return to window"
                )
                if result is None:
                    # Cancel
                    return
                if result is True:
                    # Save then quit
                    self.on_save_all_changes()
                # If result is False, fall through and quit without saving
        except Exception:
            # If any issue occurs, proceed to normal quit
            pass
        # Cleanup temp crops
        for crop in list(self.temp_crops):
            try:
                if os.path.exists(crop):
                    os.remove(crop)
            except Exception:
                pass
        self.temp_crops.clear()

    def on_save_all_changes(self):
        """Save all unmatched faces to database"""
        # Use global unmatched_faces set; commit all across people
        if not self.unmatched_faces:
            messagebox.showinfo("Nothing to Save", "There are no pending changes to save.")
            return
        result = messagebox.askyesno(
            "Confirm Save",
            f"Unlink {len(self.unmatched_faces)} face(s) across all people?\n\nThis will make them unidentified."
        )
        if not result:
            return
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            for face_id in self.unmatched_faces:
                cursor.execute('UPDATE faces SET person_id = NULL WHERE id = ?', (face_id,))
            conn.commit()
        count = len(self.unmatched_faces)
        self.unmatched_faces.clear()
        # Refresh people list and right panel for current selection
        self.load_people()
        self.populate_people_list()
        if self.current_person_id is not None and self.current_person_name:
            self.show_person_faces(self.current_person_id, self.current_person_name)
        messagebox.showinfo("Changes Saved", f"Successfully unlinked {count} face(s).")


class DashboardGUI:
    """Unified Dashboard with menu bar and content area for all features.
    
    Designed to be web-migration friendly with clear separation between
    navigation (menu bar) and content (panels).
    """

    def __init__(self, gui_core: GUICore, db_manager=None, face_processor=None, on_scan=None, on_process=None, on_identify=None, search_stats=None, tag_manager=None):
        self.gui_core = gui_core
        self.db_manager = db_manager
        self.face_processor = face_processor
        self.on_scan = on_scan
        self.on_process = on_process
        self.on_identify = on_identify
        self.search_stats = search_stats
        self.tag_manager = tag_manager
        
        # Panel management for future web migration
        self.panels: Dict[str, ttk.Frame] = {}
        self.current_panel: Optional[str] = None
        self.root: Optional[tk.Tk] = None

    def open(self) -> int:
        """Open the unified dashboard with menu bar and content area"""
        self.root = tk.Tk()
        self.root.title("PunimTag - Unified Dashboard")
        self.root.resizable(True, True)
        self.root.withdraw()

        # Make window full screen - use geometry instead of state for better compatibility
        try:
            # Try Windows-style maximized state first
            self.root.state('zoomed')
        except tk.TclError:
            try:
                # Try Linux-style maximized attribute
                self.root.attributes('-zoomed', True)
            except tk.TclError:
                # Fallback: set geometry to screen size
                screen_width = self.root.winfo_screenwidth()
                screen_height = self.root.winfo_screenheight()
                self.root.geometry(f"{screen_width}x{screen_height}+0+0")
        
        # Get screen dimensions for dynamic sizing
        screen_width = self.root.winfo_screenwidth()
        screen_height = self.root.winfo_screenheight()
        
        # Set minimum window size
        self.root.minsize(800, 600)

        # Create main container with proper grid configuration
        main_container = ttk.Frame(self.root)
        main_container.pack(fill=tk.BOTH, expand=True)
        
        # Configure main container grid weights for responsiveness
        main_container.columnconfigure(0, weight=1)
        main_container.rowconfigure(0, weight=0)  # Menu bar - fixed height
        main_container.rowconfigure(1, weight=0)  # Separator - fixed height
        main_container.rowconfigure(2, weight=1)  # Content area - expandable
        
        # Add window resize handler for dynamic responsiveness
        self.root.bind('<Configure>', self._on_window_resize)

        # Create menu bar
        self._create_menu_bar(main_container)
        
        # Create content area
        self._create_content_area(main_container)
        
        # Initialize panels
        self._initialize_panels()
        
        # Show default panel
        self.show_panel("home")

        # Show window
        self.root.deiconify()
        self.root.mainloop()
        return 0

    def _create_menu_bar(self, parent: ttk.Frame):
        """Create the top menu bar with all functionality buttons"""
        menu_frame = ttk.Frame(parent)
        menu_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=15, pady=10)
        menu_frame.columnconfigure(0, weight=0)  # Title - fixed width
        menu_frame.columnconfigure(1, weight=1)  # Buttons - expandable
        menu_frame.columnconfigure(2, weight=0)  # Status - fixed width
        
        # Title with larger font for full screen
        title_label = tk.Label(menu_frame, text="PunimTag", font=("Arial", 20, "bold"))
        title_label.grid(row=0, column=0, padx=(0, 30), sticky=tk.W)
        
        # Create buttons frame for better organization
        buttons_frame = ttk.Frame(menu_frame)
        buttons_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=20)
        
        # Menu buttons with larger size for full screen
        menu_buttons = [
            ("📁 Scan", "scan", "Scan folders and add photos"),
            ("🔍 Process", "process", "Detect faces in photos"),
            ("👤 Identify", "identify", "Identify faces in photos"),
            ("🔗 Auto-Match", "auto_match", "Find and confirm matching faces"),
            ("🔎 Search", "search", "Search photos by person name"),
            ("✏️ Modify", "modify", "View and modify identified faces"),
            ("🏷️ Tags", "tags", "Manage photo tags"),
        ]
        
        for i, (text, panel_name, tooltip) in enumerate(menu_buttons):
            btn = ttk.Button(
                buttons_frame, 
                text=text, 
                command=lambda p=panel_name: self.show_panel(p),
                width=12  # Fixed width for consistent layout
            )
            btn.grid(row=0, column=i, padx=3, sticky=tk.W)
            
            # Add tooltip functionality
            self._add_tooltip(btn, tooltip)
        
        # Status/Info area with better styling
        status_frame = ttk.Frame(menu_frame)
        status_frame.grid(row=0, column=2, sticky=tk.E, padx=(20, 0))
        
        self.status_label = tk.Label(status_frame, text="Ready", foreground="#666", font=("Arial", 10))
        self.status_label.pack(side=tk.RIGHT)
        
        # Add a subtle separator line below the menu
        separator = ttk.Separator(parent, orient='horizontal')
        separator.grid(row=1, column=0, sticky=(tk.W, tk.E), padx=15, pady=(0, 5))

    def _create_content_area(self, parent: ttk.Frame):
        """Create the main content area where panels will be displayed"""
        self.content_frame = ttk.Frame(parent)
        self.content_frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=15, pady=(0, 15))
        
        # Configure content frame to expand both horizontally and vertically
        self.content_frame.columnconfigure(0, weight=1)
        self.content_frame.rowconfigure(0, weight=1)
        
        # Add a subtle border
        self.content_frame.configure(relief='sunken', borderwidth=1)

    def _initialize_panels(self):
        """Initialize all panels (currently placeholders)"""
        # Home panel (default)
        self.panels["home"] = self._create_home_panel()
        
        # Functional panels (placeholders for now)
        self.panels["scan"] = self._create_scan_panel()
        self.panels["process"] = self._create_process_panel()
        self.panels["identify"] = self._create_identify_panel()
        self.panels["auto_match"] = self._create_auto_match_panel()
        self.panels["search"] = self._create_search_panel()
        self.panels["modify"] = self._create_modify_panel()
        self.panels["tags"] = self._create_tags_panel()

    def show_panel(self, panel_name: str):
        """Show the specified panel in the content area"""
        if panel_name not in self.panels:
            messagebox.showerror("Error", f"Panel '{panel_name}' not found", parent=self.root)
            return
        
        # Deactivate current panel if it has activation/deactivation methods
        if self.current_panel:
            self.panels[self.current_panel].grid_remove()
            # Deactivate identify panel if it's active
            if hasattr(self, 'identify_panel') and self.identify_panel and self.current_panel == "identify":
                self.identify_panel.deactivate()
            # Deactivate auto-match panel if it's active
            if hasattr(self, 'auto_match_panel') and self.auto_match_panel and self.current_panel == "auto_match":
                self.auto_match_panel.deactivate()
        
        # Show new panel - expand both horizontally and vertically
        self.panels[panel_name].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=15, pady=15)
        self.current_panel = panel_name
        
        # Activate new panel if it has activation/deactivation methods
        if panel_name == "identify" and hasattr(self, 'identify_panel') and self.identify_panel:
            self.identify_panel.activate()
        elif panel_name == "auto_match" and hasattr(self, 'auto_match_panel') and self.auto_match_panel:
            self.auto_match_panel.activate()
        
        # Update status
        self.status_label.config(text=f"Viewing: {panel_name.replace('_', ' ').title()}")

    def _add_tooltip(self, widget, text):
        """Add a simple tooltip to a widget"""
        def show_tooltip(event):
            tooltip = tk.Toplevel()
            tooltip.wm_overrideredirect(True)
            tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}")
            label = ttk.Label(tooltip, text=text, background="#ffffe0", relief="solid", borderwidth=1)
            label.pack()
            widget.tooltip = tooltip
        
        def hide_tooltip(event):
            if hasattr(widget, 'tooltip'):
                widget.tooltip.destroy()
                del widget.tooltip
        
        widget.bind("<Enter>", show_tooltip)
        widget.bind("<Leave>", hide_tooltip)

    def _on_window_resize(self, event):
        """Handle window resize events for dynamic responsiveness"""
        # Only handle resize events for the main window, not child widgets
        if event.widget == self.root:
            # Update status with current window size
            width = self.root.winfo_width()
            height = self.root.winfo_height()
            self.status_label.config(text=f"Ready - {width}x{height}")
            
            # Force update of all panels to ensure proper resizing
            if hasattr(self, 'identify_panel') and self.identify_panel:
                # Update identify panel layout if it's active
                if self.current_panel == "identify":
                    self.identify_panel.update_layout()

    def _create_home_panel(self) -> ttk.Frame:
        """Create the home/welcome panel"""
        panel = ttk.Frame(self.content_frame)
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        # Remove weight=1 from row to prevent empty space expansion
        
        # Welcome content
        welcome_frame = ttk.Frame(panel)
        welcome_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N), padx=20, pady=20)
        welcome_frame.columnconfigure(0, weight=1)
        # Remove weight=1 to prevent vertical centering
        # welcome_frame.rowconfigure(0, weight=1)
        
        # Content starts at the top instead of being centered
        center_frame = ttk.Frame(welcome_frame)
        center_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N))
        center_frame.columnconfigure(0, weight=1)
        
        # Title with larger font for full screen
        title_label = tk.Label(center_frame, text="Welcome to PunimTag", font=("Arial", 32, "bold"))
        title_label.grid(row=0, column=0, pady=(0, 30))
        
        # Description with larger font
        desc_text = (
            "PunimTag is a powerful photo face recognition and tagging system.\n\n"
            "Use the menu above to access different features:\n\n"
            "• 📁 Scan - Add photos to your collection\n"
            "• 🔍 Process - Detect faces in photos\n"
            "• 👤 Identify - Identify people in photos\n"
            "• 🔗 Auto-Match - Find matching faces automatically\n"
            "• 🔎 Search - Find photos by person name\n"
            "• ✏️ Modify - Edit face identifications\n"
            "• 🏷️ Tags - Manage photo tags\n\n"
            "Select a feature from the menu to get started!"
        )
        
        desc_label = tk.Label(center_frame, text=desc_text, font=("Arial", 14), justify=tk.LEFT)
        desc_label.grid(row=1, column=0, pady=(0, 20))
        
        return panel

    def _create_scan_panel(self) -> ttk.Frame:
        """Create the scan panel (migrated from original dashboard)"""
        panel = ttk.Frame(self.content_frame)
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        # Remove weight=1 from row to prevent empty space expansion
        
        # Title with larger font for full screen
        title_label = tk.Label(panel, text="📁 Scan Photos", font=("Arial", 24, "bold"))
        title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20))
        
        # Scan form
        form_frame = ttk.LabelFrame(panel, text="Scan Configuration", padding="20")
        form_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 20))
        form_frame.columnconfigure(0, weight=1)
        
        # Folder selection
        folder_frame = ttk.Frame(form_frame)
        folder_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15))
        folder_frame.columnconfigure(0, weight=1)
        
        tk.Label(folder_frame, text="Folder to scan:", font=("Arial", 12)).grid(row=0, column=0, sticky=tk.W)
        
        folder_input_frame = ttk.Frame(folder_frame)
        folder_input_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(5, 0))
        folder_input_frame.columnconfigure(0, weight=1)
        
        self.folder_var = tk.StringVar()
        folder_entry = tk.Entry(folder_input_frame, textvariable=self.folder_var, font=("Arial", 11))
        folder_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
        
        def browse_folder():
            from tkinter import filedialog
            folder_path = filedialog.askdirectory(title="Select folder to scan for photos")
            if folder_path:
                self.folder_var.set(folder_path)
        
        browse_btn = ttk.Button(folder_input_frame, text="Browse", command=browse_folder)
        browse_btn.grid(row=0, column=1)
        
        # Recursive option
        self.recursive_var = tk.BooleanVar(value=True)
        recursive_check = tk.Checkbutton(
            form_frame,
            text="Include photos in sub-folders",
            variable=self.recursive_var,
            font=("Arial", 11)
        )
        recursive_check.grid(row=1, column=0, sticky=tk.W, pady=(15, 0))
        
        # Action button
        scan_btn = ttk.Button(form_frame, text="🔍 Start Scan", command=self._run_scan)
        scan_btn.grid(row=2, column=0, sticky=tk.W, pady=(20, 0))
        
        return panel

    def _create_process_panel(self) -> ttk.Frame:
        """Create the process panel (migrated from original dashboard)"""
        panel = ttk.Frame(self.content_frame)
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        # Remove weight=1 from row to prevent empty space expansion
        
        # Title with larger font for full screen
        title_label = tk.Label(panel, text="🔍 Process Faces", font=("Arial", 24, "bold"))
        title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20))
        
        # Process form
        form_frame = ttk.LabelFrame(panel, text="Processing Configuration", padding="20")
        form_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 20))
        form_frame.columnconfigure(0, weight=1)
        
        # Limit option
        limit_frame = ttk.Frame(form_frame)
        limit_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15))
        
        self.limit_enabled = tk.BooleanVar(value=False)
        limit_check = tk.Checkbutton(limit_frame, text="Limit processing to", variable=self.limit_enabled, font=("Arial", 11))
        limit_check.grid(row=0, column=0, sticky=tk.W)
        
        self.limit_var = tk.StringVar(value="50")
        limit_entry = tk.Entry(limit_frame, textvariable=self.limit_var, width=8, font=("Arial", 11))
        limit_entry.grid(row=0, column=1, padx=(10, 5))
        
        tk.Label(limit_frame, text="photos", font=("Arial", 11)).grid(row=0, column=2, sticky=tk.W)
        
        # Action button
        process_btn = ttk.Button(form_frame, text="🚀 Start Processing", command=self._run_process)
        process_btn.grid(row=1, column=0, sticky=tk.W, pady=(20, 0))
        
        return panel

    def _create_identify_panel(self) -> ttk.Frame:
        """Create the identify panel with full functionality"""
        panel = ttk.Frame(self.content_frame)
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        # Configure rows: title (row 0) fixed, identify content (row 1) should expand
        panel.rowconfigure(0, weight=0)
        panel.rowconfigure(1, weight=1)
        
        # Title with larger font for full screen
        title_label = tk.Label(panel, text="👤 Identify Faces", font=("Arial", 24, "bold"))
        title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20))
        
        # Create the identify panel if we have the required dependencies
        if self.db_manager and self.face_processor:
            self.identify_panel = IdentifyPanel(panel, self.db_manager, self.face_processor, self.gui_core)
            identify_frame = self.identify_panel.create_panel()
            identify_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        else:
            # Fallback placeholder if dependencies are not available
            placeholder_frame = ttk.LabelFrame(panel, text="Configuration Required", padding="20")
            placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N))
            placeholder_frame.columnconfigure(0, weight=1)
            # Remove weight=1 to prevent vertical centering
            # placeholder_frame.rowconfigure(0, weight=1)
            
            placeholder_text = (
                "Identify panel requires database and face processor to be configured.\n\n"
                "This will contain the full face identification interface\n"
                "currently available in the separate Identify window.\n\n"
                "Features will include:\n"
                "• Face browsing and identification\n"
                "• Similar face matching\n"
                "• Person management\n"
                "• Batch processing options"
            )
            
            placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14), justify=tk.LEFT)
            placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        return panel

    def _create_auto_match_panel(self) -> ttk.Frame:
        """Create the auto-match panel with full functionality"""
        panel = ttk.Frame(self.content_frame)
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        # Configure rows: title (row 0) fixed, auto-match content (row 1) should expand
        panel.rowconfigure(0, weight=0)
        panel.rowconfigure(1, weight=1)
        
        # Title with larger font for full screen
        title_label = tk.Label(panel, text="🔗 Auto-Match Faces", font=("Arial", 24, "bold"))
        title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20))
        
        # Create the auto-match panel if we have the required dependencies
        if self.db_manager and self.face_processor:
            self.auto_match_panel = AutoMatchPanel(panel, self.db_manager, self.face_processor, self.gui_core)
            auto_match_frame = self.auto_match_panel.create_panel()
            auto_match_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        else:
            # Fallback placeholder if dependencies are not available
            placeholder_frame = ttk.LabelFrame(panel, text="Configuration Required", padding="20")
            placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N))
            placeholder_frame.columnconfigure(0, weight=1)
            # Remove weight=1 to prevent vertical centering
            # placeholder_frame.rowconfigure(0, weight=1)
            
            placeholder_text = (
                "Auto-Match panel requires database and face processor to be configured.\n\n"
                "This will contain the full auto-match interface\n"
                "currently available in the separate Auto-Match window.\n\n"
                "Features will include:\n"
                "• Person-centric matching workflow\n"
                "• Visual confirmation of matches\n"
                "• Batch identification of similar faces\n"
                "• Search and filter by person name\n"
                "• Smart pre-selection of previously identified faces"
            )
            
            placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14), justify=tk.LEFT)
            placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        return panel

    def _create_search_panel(self) -> ttk.Frame:
        """Create the search panel with full functionality"""
        panel = ttk.Frame(self.content_frame)
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        # Configure rows: title (row 0) fixed, search content (row 1) should expand
        panel.rowconfigure(0, weight=0)
        panel.rowconfigure(1, weight=1)
        
        # Title with larger font for full screen
        title_label = tk.Label(panel, text="🔎 Search Photos", font=("Arial", 24, "bold"))
        title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20))
        
        # Create the search panel if we have the required dependencies
        if self.db_manager and self.search_stats:
            self.search_panel = SearchPanel(panel, self.db_manager, self.search_stats, self.gui_core, self.tag_manager)
            search_frame = self.search_panel.create_panel()
            search_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        else:
            # Fallback placeholder if dependencies are not available
            placeholder_frame = ttk.LabelFrame(panel, text="Configuration Required", padding="20")
            placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N))
            placeholder_frame.columnconfigure(0, weight=1)
            # Remove weight=1 to prevent vertical centering
            # placeholder_frame.rowconfigure(0, weight=1)
            
            placeholder_text = (
                "Search panel requires database and search stats to be configured.\n\n"
                "This will contain the full search interface\n"
                "currently available in the separate Search window.\n\n"
                "Features will include:\n"
                "• Search photos by person name\n"
                "• Search photos by date range\n"
                "• Search photos by tags\n"
                "• Find photos without faces\n"
                "• Find photos without tags\n"
                "• Advanced filtering options"
            )
            
            placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14), justify=tk.LEFT)
            placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        return panel

    def _create_modify_panel(self) -> ttk.Frame:
        """Create the modify panel with full functionality"""
        panel = ttk.Frame(self.content_frame)
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        # Configure rows: title (row 0) fixed, modify content (row 1) should expand
        panel.rowconfigure(0, weight=0)
        panel.rowconfigure(1, weight=1)
        
        # Title with larger font for full screen
        title_label = tk.Label(panel, text="✏️ Modify Identified", font=("Arial", 24, "bold"))
        title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20))
        
        # Create the modify panel if we have the required dependencies
        if self.db_manager and self.face_processor:
            self.modify_panel = ModifyPanel(panel, self.db_manager, self.face_processor, self.gui_core)
            modify_frame = self.modify_panel.create_panel()
            modify_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        else:
            # Fallback placeholder if dependencies are not available
            placeholder_frame = ttk.LabelFrame(panel, text="Configuration Required", padding="20")
            placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N))
            placeholder_frame.columnconfigure(0, weight=1)
            
            placeholder_text = (
                "Modify panel requires database and face processor to be configured.\n\n"
                "This will contain the full modify interface\n"
                "currently available in the separate Modify window.\n\n"
                "Features will include:\n"
                "• View and edit identified people\n"
                "• Rename people across all photos\n"
                "• Unmatch faces from people\n"
                "• Bulk operations for face management"
            )
            
            placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14), justify=tk.LEFT)
            placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        return panel

    def _create_tags_panel(self) -> ttk.Frame:
        """Create the tags panel (placeholder)"""
        panel = ttk.Frame(self.content_frame)
        
        # Configure panel grid for responsiveness
        panel.columnconfigure(0, weight=1)
        # Remove weight=1 from row to prevent empty space expansion
        
        title_label = tk.Label(panel, text="🏷️ Tag Manager", font=("Arial", 24, "bold"))
        title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20))
        
        placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
        placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N))
        placeholder_frame.columnconfigure(0, weight=1)
        # Remove weight=1 to prevent vertical centering
        # placeholder_frame.rowconfigure(0, weight=1)
        
        placeholder_text = "Tag management functionality will be integrated here."
        placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14))
        placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N))
        
        return panel

    def _run_scan(self):
        """Run the scan operation (migrated from original dashboard)"""
        folder = self.folder_var.get().strip()
        recursive = bool(self.recursive_var.get())
        
        if not folder:
            messagebox.showwarning("Scan", "Please enter a folder path.", parent=self.root)
            return
        
        # Validate folder path using path utilities
        from path_utils import validate_path_exists, normalize_path
        try:
            folder = normalize_path(folder)
            if not validate_path_exists(folder):
                messagebox.showerror("Scan", f"Folder does not exist or is not accessible: {folder}", parent=self.root)
                return
        except ValueError as e:
            messagebox.showerror("Scan", f"Invalid folder path: {e}", parent=self.root)
            return
        
        if not callable(self.on_scan):
            messagebox.showinfo("Scan", "Scan functionality is not wired yet.", parent=self.root)
            return

        def worker():
            try:
                self.status_label.config(text="Scanning...")
                result = self.on_scan(folder, recursive)
                messagebox.showinfo("Scan", f"Scan completed. Result: {result}", parent=self.root)
                self.status_label.config(text="Ready")
            except Exception as e:
                messagebox.showerror("Scan", f"Error during scan: {e}", parent=self.root)
                self.status_label.config(text="Ready")

        threading.Thread(target=worker, daemon=True).start()

    def _run_process(self):
        """Run the process operation (migrated from original dashboard)"""
        if not callable(self.on_process):
            messagebox.showinfo("Process", "Process functionality is not wired yet.", parent=self.root)
            return
        
        limit_value = None
        if self.limit_enabled.get():
            try:
                limit_value = int(self.limit_var.get().strip())
                if limit_value <= 0:
                    raise ValueError
            except Exception:
                messagebox.showerror("Process", "Please enter a valid positive integer for limit.", parent=self.root)
                return

        def worker():
            try:
                self.status_label.config(text="Processing...")
                result = self.on_process(limit_value)
                messagebox.showinfo("Process", f"Processing completed. Result: {result}", parent=self.root)
                self.status_label.config(text="Ready")
            except Exception as e:
                messagebox.showerror("Process", f"Error during processing: {e}", parent=self.root)
                self.status_label.config(text="Ready")

        threading.Thread(target=worker, daemon=True).start()


