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

import os
import sys
import tkinter as tk
from tkinter import ttk, messagebox
from typing import List

from gui_core import GUICore
from search_stats import SearchStats
from database import DatabaseManager
from tag_management import TagManager


class SearchGUI:
    """GUI for searching photos by different criteria."""

    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, db_manager: DatabaseManager, search_stats: SearchStats, gui_core: GUICore, tag_manager: TagManager = None, verbose: int = 0):
        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 search_gui(self) -> int:
        """Open the Search GUI window."""
        root = tk.Tk()
        root.title("Search Photos")
        root.resizable(True, True)

        # Hide to center and size
        root.withdraw()

        main = ttk.Frame(root, padding="10")
        main.pack(fill=tk.BOTH, expand=True)

        # Search type selector
        type_frame = ttk.Frame(main)
        type_frame.pack(fill=tk.X)
        ttk.Label(type_frame, text="Search type:", font=("Arial", 10, "bold")).pack(side=tk.LEFT)
        search_type_var = tk.StringVar(value=self.SEARCH_TYPES[0])
        type_combo = ttk.Combobox(type_frame, textvariable=search_type_var, values=self.SEARCH_TYPES, state="readonly")
        type_combo.pack(side=tk.LEFT, padx=(8, 0), fill=tk.X, expand=True)

        # Filters area with expand/collapse functionality
        filters_container = ttk.LabelFrame(main, text="", padding="8")
        filters_container.pack(fill=tk.X, pady=(10, 6))
        
        # Filters header with toggle text
        filters_header = ttk.Frame(filters_container)
        filters_header.pack(fill=tk.X)
        
        # Toggle text for expand/collapse
        filters_expanded = tk.BooleanVar(value=False)  # Start collapsed
        
        def toggle_filters():
            if filters_expanded.get():
                # Collapse filters
                filters_content.pack_forget()
                toggle_text.config(text="+")
                filters_expanded.set(False)
                update_toggle_tooltip()
            else:
                # Expand filters
                filters_content.pack(fill=tk.X, pady=(4, 0))
                toggle_text.config(text="-")
                filters_expanded.set(True)
                update_toggle_tooltip()
        
        def update_toggle_tooltip():
            """Update tooltip text based on current state"""
            if 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.pack(side=tk.LEFT)
        
        toggle_text = ttk.Label(filters_header, text="+", font=("Arial", 10, "bold"), cursor="hand2")
        toggle_text.pack(side=tk.LEFT, 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)
        
        # Folder location filter
        folder_filter_frame = ttk.Frame(filters_content)
        folder_filter_frame.pack(fill=tk.X, pady=(0, 4))
        ttk.Label(folder_filter_frame, text="Folder location:").pack(side=tk.LEFT)
        folder_var = tk.StringVar()
        folder_entry = ttk.Entry(folder_filter_frame, textvariable=folder_var, width=40)
        folder_entry.pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True)
        ttk.Label(folder_filter_frame, text="(optional - filter by folder path)").pack(side=tk.LEFT, 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)
                    folder_var.set(normalized_path)
                except ValueError as e:
                    messagebox.showerror("Invalid Path", f"Invalid folder path: {e}", parent=root)
        
        browse_btn = ttk.Button(folder_filter_frame, text="Browse", command=browse_folder)
        browse_btn.pack(side=tk.LEFT, padx=(6, 0))
        
        # Clear folder filter button
        def clear_folder_filter():
            folder_var.set("")
        
        clear_folder_btn = ttk.Button(folder_filter_frame, text="Clear", command=clear_folder_filter)
        clear_folder_btn.pack(side=tk.LEFT, padx=(6, 0))
        
        # Apply filters button
        apply_filters_btn = ttk.Button(filters_content, text="Apply filters", command=lambda: do_search())
        apply_filters_btn.pack(pady=(8, 0))

        # Inputs area
        inputs = ttk.Frame(main)
        inputs.pack(fill=tk.X, pady=(10, 6))

        # Name search input
        name_frame = ttk.Frame(inputs)
        ttk.Label(name_frame, text="Person name:").pack(side=tk.LEFT)
        name_var = tk.StringVar()
        name_entry = ttk.Entry(name_frame, textvariable=name_var)
        name_entry.pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True)

        # Tag search input
        tag_frame = ttk.Frame(inputs)
        ttk.Label(tag_frame, text="Tags:").pack(side=tk.LEFT)
        tag_var = tk.StringVar()
        tag_entry = ttk.Entry(tag_frame, textvariable=tag_var)
        tag_entry.pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True)
        
        # Help icon for available tags (will be defined after tooltip functions)
        tag_help_icon = ttk.Label(tag_frame, text="❓", font=("Arial", 10), cursor="hand2")
        tag_help_icon.pack(side=tk.LEFT, padx=(6, 0))
        
        ttk.Label(tag_frame, text="(comma-separated)").pack(side=tk.LEFT, padx=(6, 0))
        
        # Tag search mode
        tag_mode_frame = ttk.Frame(inputs)
        ttk.Label(tag_mode_frame, text="Match mode:").pack(side=tk.LEFT)
        tag_mode_var = tk.StringVar(value="ANY")
        tag_mode_combo = ttk.Combobox(tag_mode_frame, textvariable=tag_mode_var, 
                                     values=["ANY", "ALL"], state="readonly", width=8)
        tag_mode_combo.pack(side=tk.LEFT, padx=(6, 0))
        ttk.Label(tag_mode_frame, text="(ANY = photos with any tag, ALL = photos with all tags)").pack(side=tk.LEFT, padx=(6, 0))

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

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

        # Results area
        results_frame = ttk.Frame(main)
        results_frame.pack(fill=tk.BOTH, expand=True)
        
        # Results header with count
        results_header = ttk.Frame(results_frame)
        results_header.pack(fill=tk.X)
        results_label = ttk.Label(results_header, text="Results:", font=("Arial", 10, "bold"))
        results_label.pack(side=tk.LEFT)
        results_count_label = ttk.Label(results_header, text="(0 items)", font=("Arial", 10), foreground="gray")
        results_count_label.pack(side=tk.LEFT, padx=(6, 0))

        columns = ("select", "person", "tags", "processed", "open_dir", "open_photo", "path", "date_taken")
        tree = ttk.Treeview(results_frame, columns=columns, show="headings", selectmode="browse")
        tree.heading("select", text="☑")
        tree.heading("person", text="Person", command=lambda: sort_treeview("person"))
        tree.heading("tags", text="Tags", command=lambda: sort_treeview("tags"))
        tree.heading("processed", text="Processed", command=lambda: sort_treeview("processed"))
        tree.heading("open_dir", text="📁")
        tree.heading("open_photo", text="👤")
        tree.heading("path", text="Photo path", command=lambda: sort_treeview("path"))
        tree.heading("date_taken", text="Date Taken", command=lambda: sort_treeview("date_taken"))
        tree.column("select", width=50, anchor="center")
        tree.column("person", width=180, anchor="w")
        tree.column("tags", width=200, anchor="w")
        tree.column("processed", width=80, anchor="center")
        tree.column("open_dir", width=50, anchor="center")
        tree.column("open_photo", width=50, anchor="center")
        tree.column("path", width=400, anchor="w")
        tree.column("date_taken", width=100, anchor="center")
        
        # Add vertical scrollbar for the treeview
        tree_v_scrollbar = ttk.Scrollbar(results_frame, orient="vertical", command=tree.yview)
        tree.configure(yscrollcommand=tree_v_scrollbar.set)
        
        # Pack treeview and scrollbar
        tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, pady=(4, 0))
        tree_v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=(4, 0))

        # Buttons
        btns = ttk.Frame(main)
        btns.pack(fill=tk.X, pady=(8, 0))
        search_btn = ttk.Button(btns, text="Search", command=lambda: do_search())
        search_btn.pack(side=tk.LEFT)
        tag_btn = ttk.Button(btns, text="Tag selected photos", command=lambda: tag_selected_photos())
        tag_btn.pack(side=tk.LEFT, padx=(6, 0))
        clear_btn = ttk.Button(btns, text="Clear all selected", command=lambda: clear_all_selected())
        clear_btn.pack(side=tk.LEFT, padx=(6, 0))
        close_btn = ttk.Button(btns, text="Close", command=lambda: root.destroy())
        close_btn.pack(side=tk.RIGHT)

        # Sorting functionality
        def sort_treeview(col: str):
            """Sort the treeview by the specified column."""
            # Get all items and their values
            items = [(tree.set(child, col), child) for child in 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):
                tree.move(child, '', index)
            
            # Update header display
            update_header_display()
        
        def update_header_display():
            """Update header display to show sort indicators."""
            # Reset all headers
            tree.heading("person", text="Person")
            tree.heading("tags", text="Tags")
            tree.heading("processed", text="Processed")
            tree.heading("path", text="Photo path")
            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 " ↑"
                tree.heading("person", text="Person" + indicator)
            elif self.sort_column == "tags":
                indicator = " ↓" if self.sort_reverse else " ↑"
                tree.heading("tags", text="Tags" + indicator)
            elif self.sort_column == "processed":
                indicator = " ↓" if self.sort_reverse else " ↑"
                tree.heading("processed", text="Processed" + indicator)
            elif self.sort_column == "path":
                indicator = " ↓" if self.sort_reverse else " ↑"
                tree.heading("path", text="Photo path" + indicator)
            elif self.sort_column == "date_taken":
                indicator = " ↓" if self.sort_reverse else " ↑"
                tree.heading("date_taken", text="Date Taken" + indicator)

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

        def filter_results_by_folder(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():
            for i in tree.get_children():
                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
            results_count_label.config(text="(0 items)")
            update_header_display()

        def add_results(rows: List[tuple]):
            # 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 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 = get_photo_tags_for_display(path)
                        processed_status = get_photo_processed_status(path)
                        tree.insert("", tk.END, values=("☐", "", photo_tags, processed_status, "📁", "👤", path, date_taken))
                    elif 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 = get_photo_tags_for_display(path)
                        date_taken = get_photo_date_taken(path)
                        processed_status = get_photo_processed_status(path)
                        tree.insert("", tk.END, values=("☐", "", photo_tags, processed_status, "📁", "👤", path, date_taken))
                    elif 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 = get_photo_tags_for_display(path)
                        date_taken = get_photo_date_taken(path)
                        processed_status = get_photo_processed_status(path)
                        tree.insert("", tk.END, values=("☐", "", photo_tags, processed_status, "📁", "", path, date_taken))
                    elif 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 = get_photo_tags_for_display(path)  # Will be "No tags"
                        date_taken = get_photo_date_taken(path)
                        processed_status = get_photo_processed_status(path)
                        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 = get_photo_tags_for_display(p)
                        date_taken = get_photo_date_taken(p)
                        processed_status = get_photo_processed_status(p)
                        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 search_type_var.get() == self.SEARCH_TYPES[1]:  # Date search
                    # Sort by date_taken column for date search
                    self.sort_column = "date_taken"
                elif search_type_var.get() == self.SEARCH_TYPES[2]:  # Tag search
                    # Sort by tags column for tag search
                    self.sort_column = "tags"
                elif search_type_var.get() == self.SEARCH_TYPES[6]:  # Photos without faces
                    # Sort by path column for photos without faces
                    self.sort_column = "path"
                elif 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 = [(tree.set(child, self.sort_column), child) for child in 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):
                    tree.move(child, '', index)
                # Update header display
                update_header_display()
            
            # Update results count
            item_count = len(tree.get_children())
            results_count_label.config(text=f"({item_count} items)")

        def do_search():
            clear_results()
            choice = search_type_var.get()
            folder_filter = folder_var.get().strip()
            
            if choice == self.SEARCH_TYPES[0]:  # Search photos by name
                query = name_var.get().strip()
                if not query:
                    messagebox.showinfo("Search", "Please enter a name to search.", parent=root)
                    return
                rows = self.search_stats.search_faces(query)
                # Apply folder filter
                rows = 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=root)
                add_results(rows)
            elif choice == self.SEARCH_TYPES[1]:  # Search photos by date
                date_from = date_from_var.get().strip()
                date_to = 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=root)
                        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=root)
                        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=root)
                    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 = 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=root)
                else:
                    # Convert to the format expected by add_results: (path, date_taken)
                    formatted_rows = [(path, date_taken) for path, date_taken in rows]
                    add_results(formatted_rows)
            elif choice == self.SEARCH_TYPES[2]:  # Search photos by tags
                tag_query = tag_var.get().strip()
                if not tag_query:
                    messagebox.showinfo("Search", "Please enter tags to search for.", parent=root)
                    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=root)
                    return
                
                # Determine match mode
                match_all = (tag_mode_var.get() == "ALL")
                
                rows = self.search_stats.search_photos_by_tags(tags, match_all)
                # Apply folder filter
                rows = 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=root)
                add_results(rows)
            elif choice == self.SEARCH_TYPES[6]:  # Photos without faces
                rows = self.search_stats.get_photos_without_faces()
                # Apply folder filter
                rows = 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=root)
                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]
                    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 = 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=root)
                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]
                    add_results(formatted_rows)

        def open_dir(path: str):
            try:
                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=root)

        def toggle_photo_selection(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
            tree.item(row_id, values=new_vals)

        def tag_selected_photos():
            """Open linkage dialog for selected photos."""
            if not self.selected_photos:
                messagebox.showinfo("Tag Photos", "Please select photos to tag first.", parent=root)
                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=root)
                return
            
            # Open the linkage dialog
            open_linkage_dialog(selected_photo_ids)

        def clear_all_selected():
            """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 tree.get_children():
                vals = tree.item(item, "values")
                if len(vals) >= 7 and vals[0] == "☑":
                    new_vals = list(vals)
                    new_vals[0] = "☐"
                    tree.item(item, values=new_vals)


        def show_photo_tags(photo_path):
            """Show tags for a specific photo in a popup."""
            # Get photo ID
            photo_id = None
            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 result:
                    photo_id = result[0]
            
            if not photo_id:
                messagebox.showerror("Error", "Could not find photo ID", parent=root)
                return
            
            # Get tags for this photo
            tag_names = []
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                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()]
            
            # Create popup
            popup = tk.Toplevel(root)
            popup.title("Photo Tags")
            popup.transient(root)
            popup.grab_set()
            popup.geometry("300x200")
            
            # Center the popup
            popup.update_idletasks()
            x = (popup.winfo_screenwidth() // 2) - (popup.winfo_width() // 2)
            y = (popup.winfo_screenheight() // 2) - (popup.winfo_height() // 2)
            popup.geometry(f"+{x}+{y}")
            
            frame = ttk.Frame(popup, padding="10")
            frame.pack(fill=tk.BOTH, expand=True)
            
            # Photo filename
            filename = os.path.basename(photo_path)
            ttk.Label(frame, text=f"Tags for: {filename}", font=("Arial", 10, "bold")).pack(anchor="w", pady=(0, 10))
            
            if tag_names:
                # Create scrollable list
                canvas = tk.Canvas(frame, height=100)
                scrollbar = ttk.Scrollbar(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")
                
                for tag_name in tag_names:
                    ttk.Label(scrollable_frame, text=f"• {tag_name}").pack(anchor="w", pady=1)
            else:
                ttk.Label(frame, text="No tags found for this photo", foreground="gray").pack(anchor="w")
            
            # Close button
            ttk.Button(frame, text="Close", command=popup.destroy).pack(pady=(10, 0))

        def get_person_name_for_photo(photo_path):
            """Get person name for a photo (if any faces are identified)."""
            try:
                with self.db.get_db_connection() as conn:
                    cursor = conn.cursor()
                    cursor.execute('''
                        SELECT DISTINCT pe.first_name, pe.last_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
                        LIMIT 1
                    ''', (photo_path,))
                    result = cursor.fetchone()
                    if result:
                        first = (result[0] or "").strip()
                        last = (result[1] or "").strip()
                        return f"{first} {last}".strip() or "Unknown"
            except Exception:
                pass
            return "No person identified"
        
        def get_photo_tags_for_display(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(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(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(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 get_photo_tags_tooltip(photo_path):
            """Get tags for a photo to display in tooltip."""
            # Get photo ID
            photo_id = None
            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 result:
                    photo_id = result[0]
            
            if not photo_id:
                return "No photo found"
            
            # Get tags for this photo
            tag_names = []
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                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()]
            
            if tag_names:
                if len(tag_names) <= 5:
                    return f"Tags: {', '.join(tag_names)}"
                else:
                    return f"Tags: {', '.join(tag_names[:5])}... (+{len(tag_names)-5} more)"
            else:
                return "No tags"

        def open_linkage_dialog(photo_ids):
            """Open the linkage dialog for selected photos using tag manager functionality."""
            popup = tk.Toplevel(root)
            popup.title("Tag Selected Photos")
            popup.transient(root)
            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 get_saved_tag_types_for_photo(photo_id: int):
                """Get saved linkage types for a photo {tag_id: type_int}"""
                types = {}
                try:
                    with self.db.get_db_connection() as conn:
                        cursor = conn.cursor()
                        cursor.execute('SELECT tag_id, linkage_type FROM phototaglinkage WHERE photo_id = ?', (photo_id,))
                        for row in cursor.fetchall():
                            try:
                                types[row[0]] = int(row[1]) if row[1] is not None else 0
                            except Exception:
                                types[row[0]] = 0
                except Exception:
                    pass
                return types

            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 tree.get_children():
                    vals = 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 = get_photo_tags_for_display(photo_path)
                            # Update the tags column (index 2)
                            new_vals = list(vals)
                            new_vals[2] = current_tags
                            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()

        # Click handling on icon columns
        def on_tree_click(event):
            region = tree.identify("region", event.x, event.y)
            if region != "cell":
                return
            row_id = tree.identify_row(event.y)
            col_id = tree.identify_column(event.x)  # '#1', '#2', ...
            if not row_id or not col_id:
                return
            vals = tree.item(row_id, "values")
            if not vals or len(vals) < 6:
                return
            
            # Determine column offsets based on search type
            is_name_search = (search_type_var.get() == self.SEARCH_TYPES[0])
            is_photos_without_faces = (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
                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:
                    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=root)
            elif col_id == select_col:  # Checkbox column
                toggle_photo_selection(row_id, vals)

        # Tooltip for icon cells and toggle text
        tooltip = None
        def show_tooltip(widget, x, y, text: str):
            nonlocal tooltip
            hide_tooltip()
            try:
                tooltip = tk.Toplevel(widget)
                tooltip.wm_overrideredirect(True)
                tooltip.wm_geometry(f"+{x+12}+{y+12}")
                lbl = tk.Label(tooltip, text=text, background="lightyellow", relief="solid", borderwidth=1, font=("Arial", 9))
                lbl.pack()
            except Exception:
                tooltip = None

        def hide_tooltip(*_):
            nonlocal tooltip
            if tooltip is not None:
                try:
                    tooltip.destroy()
                except Exception:
                    pass
                tooltip = None
        
        # Tooltip functionality for toggle text
        def on_toggle_enter(event):
            if hasattr(toggle_text, 'tooltip_text'):
                show_tooltip(toggle_text, event.x_root, event.y_root, toggle_text.tooltip_text)
        
        def on_toggle_leave(event):
            hide_tooltip()
        
        # Bind tooltip events to toggle text
        toggle_text.bind("<Enter>", on_toggle_enter)
        toggle_text.bind("<Leave>", on_toggle_leave)
        
        # Help icon functionality for available tags
        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"
                
                show_tooltip(tag_help_icon, event.x_root, event.y_root, tooltip_text)
            except Exception:
                show_tooltip(tag_help_icon, event.x_root, event.y_root, "Error loading tags")
        
        # Bind tooltip events to help icon
        tag_help_icon.bind("<Enter>", show_available_tags_tooltip)
        tag_help_icon.bind("<Leave>", hide_tooltip)

        def on_tree_motion(event):
            region = tree.identify("region", event.x, event.y)
            if region != "cell":
                hide_tooltip()
                tree.config(cursor="")
                return
            col_id = tree.identify_column(event.x)
            row_id = tree.identify_row(event.y)
            
            # Determine column offsets based on search type
            is_name_search = (search_type_var.get() == self.SEARCH_TYPES[0])
            is_photos_without_faces = (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
                tree.config(cursor="")
                # Show tags tooltip
                if row_id:
                    vals = 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]
                        show_tooltip(tree, event.x_root, event.y_root, f"Tags: {tags_text}")
            elif col_id == open_dir_col:  # Open directory column
                tree.config(cursor="hand2")
                show_tooltip(tree, event.x_root, event.y_root, "Open file location")
            elif col_id == face_col:  # Face icon column
                tree.config(cursor="hand2")
                # Show people tooltip
                if row_id:
                    vals = tree.item(row_id, "values")
                    if len(vals) >= 5:
                        path = vals[path_index]
                        people_text = get_photo_people_tooltip(path)
                        show_tooltip(tree, event.x_root, event.y_root, people_text)
            elif col_id == path_col:  # Photo path column
                tree.config(cursor="hand2")
                show_tooltip(tree, event.x_root, event.y_root, "Open photo")
            else:
                tree.config(cursor="")
                hide_tooltip()

        type_combo.bind("<<ComboboxSelected>>", switch_inputs)
        switch_inputs()
        tree.bind("<Button-1>", on_tree_click)
        tree.bind("<Motion>", on_tree_motion)
        tree.bind("<Leave>", hide_tooltip)

        # Enter key in name field triggers search
        name_entry.bind("<Return>", lambda e: do_search())
        # Enter key in tag field triggers search
        tag_entry.bind("<Return>", lambda e: do_search())
        # Note: Date fields are read-only, so no Enter key binding needed
        # Enter key in folder filter field triggers search
        folder_entry.bind("<Return>", lambda e: do_search())

        # Show and center
        root.update_idletasks()
        # Widened to ensure all columns are visible by default, including the new processed column
        self.gui_core.center_window(root, 1300, 520)
        root.deiconify()
        root.mainloop()
        return 0


