#!/usr/bin/env python3
"""
Integrated Tag Manager Panel for PunimTag Dashboard
Embeds the full tag manager GUI functionality into the dashboard frame
"""

import os
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from PIL import Image
try:
    from PIL import ImageTk
except ImportError:
    # Fallback for older PIL versions
    import ImageTk
from typing import List, Dict, Tuple, Optional
import sys
import subprocess

from src.core.database import DatabaseManager
from src.gui.gui_core import GUICore
from src.core.tag_management import TagManager
from src.core.face_processing import FaceProcessor


class TagManagerPanel:
    """Integrated tag manager panel that embeds the full tag manager GUI functionality into the dashboard"""
    
    def __init__(self, parent_frame: ttk.Frame, db_manager: DatabaseManager, 
                 gui_core: GUICore, tag_manager: TagManager, face_processor: FaceProcessor, on_navigate_home=None, verbose: int = 0):
        """Initialize the tag manager panel"""
        self.parent_frame = parent_frame
        self.db = db_manager
        self.gui_core = gui_core
        self.tag_manager = tag_manager
        self.face_processor = face_processor
        self.on_navigate_home = on_navigate_home
        self.verbose = verbose
        
        # Panel state
        self.is_active = False
        self.temp_crops: List[str] = []
        self.photo_images: List[ImageTk.PhotoImage] = []  # Keep PhotoImage refs alive
        self.folder_states: Dict[str, bool] = {}
        
        # Track pending tag changes/removals using tag IDs
        self.pending_tag_changes: Dict[int, List[int]] = {}
        self.pending_tag_removals: Dict[int, List[int]] = {}
        # Track linkage type for pending additions: 0=single, 1=bulk
        self.pending_tag_linkage_type: Dict[int, Dict[int, int]] = {}
        
        # Tag data
        self.existing_tags: List[str] = []
        self.tag_id_to_name: Dict[int, str] = {}
        self.tag_name_to_id: Dict[str, int] = {}
        
        # Photo data
        self.photos_data: List[Dict] = []
        self.people_names_cache: Dict[int, List[str]] = {}  # {photo_id: [list of people names]}
        
        # View configuration
        self.view_mode_var = tk.StringVar(value="list")
        self.column_visibility = {
            'list': {'id': True, 'filename': True, 'path': True, 'processed': True, 'date_taken': True, 'faces': True, 'tags': True},
            'icons': {'thumbnail': True, 'id': True, 'filename': True, 'processed': True, 'date_taken': True, 'faces': True, 'tags': True},
            'compact': {'filename': True, 'faces': True, 'tags': True}
        }
        
        # Column resizing state
        self.resize_start_x = 0
        self.resize_start_widths: List[int] = []
        self.current_visible_cols: List[Dict] = []
        self.is_resizing = False
        
        self.column_config = {
            'list': [
                {'key': 'id', 'label': 'ID', 'width': 60, 'weight': 0},
                {'key': 'filename', 'label': 'Filename', 'width': 250, 'weight': 1},
                {'key': 'path', 'label': 'Path', 'width': 400, 'weight': 2},
                {'key': 'processed', 'label': 'Processed', 'width': 100, 'weight': 0},
                {'key': 'date_taken', 'label': 'Date Taken', 'width': 140, 'weight': 0},
                {'key': 'faces', 'label': 'Faces', 'width': 80, 'weight': 0},
                {'key': 'tags', 'label': 'Tags', 'width': 300, 'weight': 1}
            ],
            'icons': [
                {'key': 'thumbnail', 'label': 'Thumbnail', 'width': 200, 'weight': 0},
                {'key': 'id', 'label': 'ID', 'width': 80, 'weight': 0},
                {'key': 'filename', 'label': 'Filename', 'width': 300, 'weight': 1},
                {'key': 'processed', 'label': 'Processed', 'width': 100, 'weight': 0},
                {'key': 'date_taken', 'label': 'Date Taken', 'width': 140, 'weight': 0},
                {'key': 'faces', 'label': 'Faces', 'width': 80, 'weight': 0},
                {'key': 'tags', 'label': 'Tags', 'width': 300, 'weight': 1}
            ],
            'compact': [
                {'key': 'filename', 'label': 'Filename', 'width': 400, 'weight': 1},
                {'key': 'faces', 'label': 'Faces', 'width': 80, 'weight': 0},
                {'key': 'tags', 'label': 'Tags', 'width': 500, 'weight': 1}
            ]
        }
        
        # GUI components
        self.components = {}
        self.main_frame = None
        
    def create_panel(self) -> ttk.Frame:
        """Create the tag manager panel with all GUI components"""
        self.main_frame = ttk.Frame(self.parent_frame)
        
        # Configure grid weights for full screen responsiveness
        self.main_frame.columnconfigure(0, weight=1)
        self.main_frame.rowconfigure(0, weight=0)  # Header - fixed height
        self.main_frame.rowconfigure(1, weight=1)  # Content area - expandable
        self.main_frame.rowconfigure(2, weight=0)  # Bottom buttons - fixed height
        
        # Create all GUI components
        self._create_header()
        self._create_content_area()
        self._create_bottom_buttons()
        
        # Load initial data
        self._load_existing_tags()
        self._load_photos()
        self._switch_view_mode("list")
        
        return self.main_frame
    
    def _create_header(self):
        """Create the header with title and view controls"""
        header_frame = ttk.Frame(self.main_frame)
        header_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        header_frame.columnconfigure(1, weight=1)
        
        # Title
        title_label = ttk.Label(header_frame, text="Photo Explorer - Tag Management", font=("Arial", 16, "bold"))
        title_label.grid(row=0, column=0, sticky=tk.W)
        
        # View controls
        view_frame = ttk.Frame(header_frame)
        view_frame.grid(row=0, column=1, sticky=tk.E)
        ttk.Label(view_frame, text="View:").pack(side=tk.LEFT, padx=(0, 5))
        ttk.Radiobutton(view_frame, text="List", variable=self.view_mode_var, value="list", 
                       command=lambda: self._switch_view_mode("list")).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Radiobutton(view_frame, text="Icons", variable=self.view_mode_var, value="icons", 
                       command=lambda: self._switch_view_mode("icons")).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Radiobutton(view_frame, text="Compact", variable=self.view_mode_var, value="compact", 
                       command=lambda: self._switch_view_mode("compact")).pack(side=tk.LEFT)
        
        # Manage Tags button
        manage_tags_btn = ttk.Button(header_frame, text="Manage Tags", command=self._open_manage_tags_dialog)
        manage_tags_btn.grid(row=0, column=2, sticky=tk.E, padx=(10, 0))
        
        self.components['header_frame'] = header_frame
        self.components['view_frame'] = view_frame
        self.components['manage_tags_btn'] = manage_tags_btn
    
    def _create_content_area(self):
        """Create the main content area with scrollable canvas"""
        content_frame = ttk.Frame(self.main_frame)
        content_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        content_frame.columnconfigure(0, weight=1)
        content_frame.rowconfigure(0, weight=1)
        
        # Get canvas background color
        style = ttk.Style()
        canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
        
        # Create scrollable content area
        self.components['content_canvas'] = tk.Canvas(content_frame, bg=canvas_bg_color, highlightthickness=0)
        self.components['content_scrollbar'] = ttk.Scrollbar(content_frame, orient="vertical", 
                                                           command=self.components['content_canvas'].yview)
        self.components['content_inner'] = ttk.Frame(self.components['content_canvas'])
        
        self.components['content_canvas'].create_window((0, 0), window=self.components['content_inner'], anchor="nw")
        self.components['content_canvas'].configure(yscrollcommand=self.components['content_scrollbar'].set)
        self.components['content_inner'].bind("<Configure>", 
                                            lambda e: self.components['content_canvas'].configure(
                                                scrollregion=self.components['content_canvas'].bbox("all")))
        
        self.components['content_canvas'].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        self.components['content_scrollbar'].grid(row=0, column=1, sticky=(tk.N, tk.S))
        
        # Bind mousewheel scrolling
        self._bind_mousewheel_scrolling()
        
        self.components['content_frame'] = content_frame
    
    def _create_bottom_buttons(self):
        """Create the bottom button area"""
        bottom_frame = ttk.Frame(self.main_frame)
        bottom_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
        
        # Save button
        self.components['save_button'] = ttk.Button(bottom_frame, text="Save Tagging", 
                                                   command=self._save_tagging_changes)
        self.components['save_button'].pack(side=tk.RIGHT, padx=10, pady=5)
        
        # Quit button (for standalone mode - not used in dashboard)
        self.components['quit_button'] = ttk.Button(bottom_frame, text="Exit Tag Manager", 
                                                   command=self._quit_with_warning)
        self.components['quit_button'].pack(side=tk.RIGHT, padx=(0, 10), pady=5)
        
        self.components['bottom_frame'] = bottom_frame
    
    def _bind_mousewheel_scrolling(self):
        """Bind mousewheel scrolling to the content canvas"""
        def on_mousewheel(event):
            self.components['content_canvas'].yview_scroll(int(-1 * (event.delta / 120)), "units")
        
        # Bind to the main frame and all its children
        self.main_frame.bind_all("<MouseWheel>", on_mousewheel)
        self.main_frame.bind_all("<ButtonRelease-1>", self._global_mouse_release)
        
        # Store reference for cleanup
        self._mousewheel_handler = on_mousewheel
    
    def _start_resize(self, event, col_idx):
        """Start column resizing"""
        self.is_resizing = True
        self.resize_start_x = event.x_root
        self.resize_start_widths = [col['width'] for col in self.current_visible_cols]
        self.main_frame.configure(cursor="sb_h_double_arrow")
    
    def _do_resize(self, event, col_idx):
        """Handle column resizing during drag"""
        if not self.is_resizing or not self.resize_start_widths or not self.current_visible_cols:
            return
        delta_x = event.x_root - self.resize_start_x
        if col_idx < len(self.current_visible_cols) and col_idx + 1 < len(self.current_visible_cols):
            new_width_left = max(50, self.resize_start_widths[col_idx] + delta_x)
            new_width_right = max(50, self.resize_start_widths[col_idx + 1] - delta_x)
            self.current_visible_cols[col_idx]['width'] = new_width_left
            self.current_visible_cols[col_idx + 1]['width'] = new_width_right
            
            # Update column config
            for i, col in enumerate(self.column_config['list']):
                if col['key'] == self.current_visible_cols[col_idx]['key']:
                    self.column_config['list'][i]['width'] = new_width_left
                elif col['key'] == self.current_visible_cols[col_idx + 1]['key']:
                    self.column_config['list'][i]['width'] = new_width_right
            
            # Update grid configuration
            try:
                header_frame_ref = None
                row_frames = []
                for widget in self.components['content_inner'].winfo_children():
                    if isinstance(widget, ttk.Frame):
                        if header_frame_ref is None:
                            header_frame_ref = widget
                        else:
                            row_frames.append(widget)
                
                if header_frame_ref is not None:
                    header_frame_ref.columnconfigure(col_idx * 2, weight=self.current_visible_cols[col_idx]['weight'], minsize=new_width_left)
                    header_frame_ref.columnconfigure((col_idx + 1) * 2, weight=self.current_visible_cols[col_idx + 1]['weight'], minsize=new_width_right)
                
                for rf in row_frames:
                    rf.columnconfigure(col_idx, weight=self.current_visible_cols[col_idx]['weight'], minsize=new_width_left)
                    rf.columnconfigure(col_idx + 1, weight=self.current_visible_cols[col_idx + 1]['weight'], minsize=new_width_right)
                
                self.main_frame.update_idletasks()
            except Exception:
                pass
    
    def _stop_resize(self, event):
        """Stop column resizing"""
        self.is_resizing = False
        self.main_frame.configure(cursor="")
    
    def _global_mouse_release(self, event):
        """Handle global mouse release for column resizing"""
        if self.is_resizing:
            self._stop_resize(event)
    
    def _show_column_context_menu(self, event, view_mode):
        """Show column context menu for show/hide columns"""
        popup = tk.Toplevel(self.main_frame)
        popup.wm_overrideredirect(True)
        popup.wm_geometry(f"+{event.x_root}+{event.y_root}")
        popup.configure(bg='white', relief='flat', bd=0)
        menu_frame = tk.Frame(popup, bg='white')
        menu_frame.pack(padx=2, pady=2)
        checkbox_vars: Dict[str, tk.BooleanVar] = {}
        protected_columns = {'icons': ['thumbnail'], 'compact': ['filename'], 'list': ['filename']}

        def close_popup():
            try:
                popup.destroy()
            except Exception:
                pass

        def close_on_click_outside(e):
            if e.widget != popup:
                try:
                    popup.winfo_exists()
                    close_popup()
                except tk.TclError:
                    pass

        for col in self.column_config[view_mode]:
            key = col['key']
            label = col['label']
            is_visible = self.column_visibility[view_mode][key]
            is_protected = key in protected_columns.get(view_mode, [])
            item_frame = tk.Frame(menu_frame, bg='white', relief='flat', bd=0)
            item_frame.pack(fill=tk.X, pady=1)
            var = tk.BooleanVar(value=is_visible)
            checkbox_vars[key] = var

            def make_toggle_command(col_key, var_ref):
                def toggle_column():
                    if col_key in protected_columns.get(view_mode, []):
                        return
                    self.column_visibility[view_mode][col_key] = var_ref.get()
                    self._switch_view_mode(view_mode)
                return toggle_column

            if is_protected:
                cb = tk.Checkbutton(item_frame, text=label, variable=var, state='disabled', 
                                  bg='white', fg='gray', font=("Arial", 9), relief='flat', 
                                  bd=0, highlightthickness=0)
                cb.pack(side=tk.LEFT, padx=5, pady=2)
                tk.Label(item_frame, text="(always visible)", bg='white', fg='gray', 
                        font=("Arial", 8)).pack(side=tk.LEFT, padx=(0, 5))
            else:
                cb = tk.Checkbutton(item_frame, text=label, variable=var, 
                                  command=make_toggle_command(key, var), bg='white', 
                                  font=("Arial", 9), relief='flat', bd=0, highlightthickness=0)
                cb.pack(side=tk.LEFT, padx=5, pady=2)

        self.main_frame.bind("<Button-1>", close_on_click_outside)
        self.main_frame.bind("<Button-3>", close_on_click_outside)
        self.components['content_canvas'].bind("<Button-1>", close_on_click_outside)
        self.components['content_canvas'].bind("<Button-3>", close_on_click_outside)
        popup.focus_set()
    
    def _unbind_mousewheel_scrolling(self):
        """Unbind mousewheel scrolling and mouse events"""
        try:
            self.main_frame.unbind_all("<MouseWheel>")
            self.main_frame.unbind_all("<ButtonRelease-1>")
        except Exception:
            pass
    
    def _load_existing_tags(self):
        """Load existing tags from database"""
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('SELECT id, tag_name FROM tags ORDER BY tag_name')
            self.existing_tags = []
            self.tag_id_to_name = {}
            self.tag_name_to_id = {}
            for row in cursor.fetchall():
                tag_id, tag_name = row
                self.existing_tags.append(tag_name)
                self.tag_id_to_name[tag_id] = tag_name
                self.tag_name_to_id[tag_name] = tag_id
    
    def _load_photos(self):
        """Load photos data from database"""
        with self.db.get_db_connection() as conn:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT p.id, p.filename, p.path, p.processed, p.date_taken, p.date_added,
                       (SELECT COUNT(*) FROM faces f WHERE f.photo_id = p.id) as face_count,
                       (SELECT GROUP_CONCAT(DISTINCT t.tag_name) 
                        FROM phototaglinkage ptl 
                        JOIN tags t ON t.id = ptl.tag_id 
                        WHERE ptl.photo_id = p.id) as tags
                FROM photos p
                ORDER BY p.date_taken DESC, p.filename
            ''')
            self.photos_data = []
            for row in cursor.fetchall():
                self.photos_data.append({
                    'id': row[0],
                    'filename': row[1],
                    'path': row[2],
                    'processed': row[3],
                    'date_taken': row[4],
                    'date_added': row[5],
                    'face_count': row[6] or 0,
                    'tags': row[7] or ""
                })
            
            # Cache people names for each photo
            self.people_names_cache.clear()
            cursor.execute('''
                SELECT f.photo_id, pe.first_name, pe.last_name
                FROM faces f
                JOIN people pe ON pe.id = f.person_id
                WHERE f.person_id IS NOT NULL
                ORDER BY pe.last_name, pe.first_name
            ''')
            cache_rows = cursor.fetchall()
            for row in cache_rows:
                photo_id, first_name, last_name = row
                if photo_id not in self.people_names_cache:
                    self.people_names_cache[photo_id] = []
                # Format as "Last, First" or just "First" if no last name
                if last_name and first_name:
                    name = f"{last_name}, {first_name}"
                else:
                    name = first_name or last_name or "Unknown"
                if name not in self.people_names_cache[photo_id]:
                    self.people_names_cache[photo_id].append(name)
    
    def _get_saved_tag_types_for_photo(self, photo_id: int) -> Dict[int, int]:
        """Get saved linkage types for a photo {tag_id: type_int}"""
        types: Dict[int, int] = {}
        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 _get_people_names_for_photo(self, photo_id: int) -> str:
        """Get people names for a photo as a formatted string for tooltip"""
        people_names = self.people_names_cache.get(photo_id, [])
        if not people_names:
            return "No people identified"
        
        # Remove commas from names (convert "Last, First" to "Last First")
        formatted_names = []
        for name in people_names:
            if ', ' in name:
                # Convert "Last, First" to "Last First"
                formatted_name = name.replace(', ', ' ')
            else:
                formatted_name = name
            formatted_names.append(formatted_name)
        
        if len(formatted_names) <= 5:
            return f"People: {', '.join(formatted_names)}"
        else:
            return f"People: {', '.join(formatted_names[:5])}... (+{len(formatted_names)-5} more)"
    
    def _open_photo(self, photo_path: str):
        """Open a photo in a preview window"""
        if not os.path.exists(photo_path):
            try:
                messagebox.showerror("File not found", f"Photo does not exist:\n{photo_path}")
            except Exception:
                pass
            return
        try:
            # Open in an in-app preview window sized reasonably compared to the main GUI
            img = Image.open(photo_path)
            screen_w = self.main_frame.winfo_screenwidth()
            screen_h = self.main_frame.winfo_screenheight()
            max_w = int(min(1000, screen_w * 0.6))
            max_h = int(min(800, screen_h * 0.6))
            preview = tk.Toplevel(self.main_frame)
            preview.title(os.path.basename(photo_path))
            preview.transient(self.main_frame)
            # Resize image to fit nicely while keeping aspect ratio
            img_copy = img.copy()
            img_copy.thumbnail((max_w, max_h), Image.Resampling.LANCZOS)
            photo_img = ImageTk.PhotoImage(img_copy)
            self.photo_images.append(photo_img)
            pad = 12
            w, h = photo_img.width(), photo_img.height()
            # Center the window roughly relative to screen
            x = int((screen_w - (w + pad)) / 2)
            y = int((screen_h - (h + pad)) / 2)
            preview.geometry(f"{w + pad}x{h + pad}+{max(x,0)}+{max(y,0)}")
            canvas = tk.Canvas(preview, width=w, height=h, highlightthickness=0)
            canvas.pack(padx=pad//2, pady=pad//2)
            canvas.create_image(w // 2, h // 2, image=photo_img)
            preview.focus_set()
        except Exception:
            # Fallback to system default opener if preview fails for any reason
            try:
                if sys.platform.startswith('linux'):
                    subprocess.Popen(['xdg-open', photo_path])
                elif sys.platform == 'darwin':
                    subprocess.Popen(['open', photo_path])
                elif os.name == 'nt':
                    os.startfile(photo_path)  # type: ignore[attr-defined]
                else:
                    Image.open(photo_path).show()
            except Exception as e:
                try:
                    messagebox.showerror("Error", f"Failed to open photo:\n{e}")
                except Exception:
                    pass
    
    def _open_manage_tags_dialog(self):
        """Open the manage tags dialog"""
        dialog = tk.Toplevel(self.main_frame)
        dialog.title("Manage Tags")
        dialog.transient(self.main_frame)
        dialog.grab_set()
        dialog.geometry("600x600")

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

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

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

        def add_new_tag():
            tag_name = new_tag_var.get().strip()
            if not tag_name:
                return
            try:
                # Use database method for case-insensitive tag creation
                tag_id = self.db.add_tag(tag_name)
                if tag_id:
                    new_tag_var.set("")
                    refresh_tag_list()
                    self._load_existing_tags()
                    self._switch_view_mode(self.view_mode_var.get())
            except Exception as e:
                messagebox.showerror("Error", f"Failed to add tag: {e}")

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

        canvas = tk.Canvas(list_frame, highlightthickness=0)
        scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
        rows_container = ttk.Frame(canvas)
        canvas.create_window((0, 0), window=rows_container, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
        scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        list_frame.columnconfigure(0, weight=1)
        list_frame.rowconfigure(0, weight=1)
        rows_container.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))

        selected_tag_vars: Dict[int, tk.BooleanVar] = {}
        current_tags: List[Dict] = []

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

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

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

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

        refresh_tag_list()

        def delete_selected():
            ids_to_delete = [tid for tid, v in selected_tag_vars.items() if v.get()]
            if not ids_to_delete:
                return
            if not self.gui_core.create_large_messagebox(self.main_frame, "Confirm Delete", f"Delete {len(ids_to_delete)} selected tag(s)? This will unlink them from photos.", "askyesno"):
                return
            try:
                with self.db.get_db_connection() as conn:
                    cursor = conn.cursor()
                    cursor.execute(f"DELETE FROM phototaglinkage WHERE tag_id IN ({','.join('?' for _ in ids_to_delete)})", ids_to_delete)
                    cursor.execute(f"DELETE FROM tags WHERE id IN ({','.join('?' for _ in ids_to_delete)})", ids_to_delete)
                    conn.commit()

                for photo_id in list(self.pending_tag_changes.keys()):
                    self.pending_tag_changes[photo_id] = [tid for tid in self.pending_tag_changes[photo_id] if tid not in ids_to_delete]
                    if not self.pending_tag_changes[photo_id]:
                        del self.pending_tag_changes[photo_id]
                for photo_id in list(self.pending_tag_removals.keys()):
                    self.pending_tag_removals[photo_id] = [tid for tid in self.pending_tag_removals[photo_id] if tid not in ids_to_delete]
                    if not self.pending_tag_removals[photo_id]:
                        del self.pending_tag_removals[photo_id]

                refresh_tag_list()
                self._load_existing_tags()
                self._load_photos()
                self._switch_view_mode(self.view_mode_var.get())
            except Exception as e:
                messagebox.showerror("Error", f"Failed to delete tags: {e}")

        ttk.Button(bottom_frame, text="Delete selected tags", command=delete_selected).pack(side=tk.LEFT)
        ttk.Button(bottom_frame, text="Close", command=dialog.destroy).pack(side=tk.RIGHT)
        new_tag_entry.focus_set()
    
    def _save_tagging_changes(self):
        """Save pending tag changes to database"""
        if not self.pending_tag_changes and not self.pending_tag_removals:
            messagebox.showinfo("Info", "No tag changes to save.")
            return
        try:
            with self.db.get_db_connection() as conn:
                cursor = conn.cursor()
                for photo_id, tag_ids in self.pending_tag_changes.items():
                    for tag_id in tag_ids:
                        lt = 0
                        try:
                            lt = self.pending_tag_linkage_type.get(photo_id, {}).get(tag_id, 0)
                        except Exception:
                            lt = 0
                        cursor.execute('''
                            INSERT INTO phototaglinkage (photo_id, tag_id, linkage_type)
                            VALUES (?, ?, ?)
                            ON CONFLICT(photo_id, tag_id) DO UPDATE SET linkage_type=excluded.linkage_type, created_date=CURRENT_TIMESTAMP
                        ''', (photo_id, tag_id, lt))
                for photo_id, tag_ids in self.pending_tag_removals.items():
                    for tag_id in tag_ids:
                        cursor.execute('DELETE FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?', (photo_id, tag_id))
                conn.commit()
            saved_additions = len(self.pending_tag_changes)
            saved_removals = len(self.pending_tag_removals)
            self.pending_tag_changes.clear()
            self.pending_tag_removals.clear()
            self.pending_tag_linkage_type.clear()
            self._load_existing_tags()
            self._load_photos()
            self._switch_view_mode(self.view_mode_var.get())
            self._update_save_button_text()
            message = f"Saved {saved_additions} tag additions"
            if saved_removals > 0:
                message += f" and {saved_removals} tag removals"
            message += "."
            messagebox.showinfo("Success", message)
        except Exception as e:
            messagebox.showerror("Error", f"Failed to save tags: {str(e)}")
    
    def _update_save_button_text(self):
        """Update the save button text to show pending changes"""
        total_additions = sum(len(tags) for tags in self.pending_tag_changes.values())
        total_removals = sum(len(tags) for tags in self.pending_tag_removals.values())
        total_changes = total_additions + total_removals
        if total_changes > 0:
            self.components['save_button'].configure(text=f"Save Tagging ({total_changes} pending)")
        else:
            self.components['save_button'].configure(text="Save Tagging")
    
    def _quit_with_warning(self):
        """Quit with warning about unsaved changes (for standalone mode)"""
        has_pending_changes = bool(self.pending_tag_changes or self.pending_tag_removals)
        if has_pending_changes:
            total_additions = sum(len(tags) for tags in self.pending_tag_changes.values())
            total_removals = sum(len(tags) for tags in self.pending_tag_removals.values())
            changes_text = []
            if total_additions > 0:
                changes_text.append(f"{total_additions} tag addition(s)")
            if total_removals > 0:
                changes_text.append(f"{total_removals} tag removal(s)")
            changes_summary = " and ".join(changes_text)
            result = self.gui_core.create_large_messagebox(
                self.main_frame, 
                "Unsaved Changes",
                f"You have unsaved changes: {changes_summary}.\n\nDo you want to save your changes before quitting?\n\nYes = Save and quit\nNo = Quit without saving\nCancel = Stay in dialog",
                "askyesnocancel"
            )
            if result is True:
                self._save_tagging_changes()
                # In dashboard mode, we don't actually quit the window
            elif result is False:
                # In dashboard mode, we don't actually quit the window
                pass
        # In dashboard mode, we don't actually quit the window
        # Navigate to home if callback is available (dashboard mode)
        if self.on_navigate_home:
            self.on_navigate_home()
    
    def _switch_view_mode(self, mode: str):
        """Switch between different view modes"""
        if mode == "list":
            self._show_list_view()
        elif mode == "icons":
            self._show_icon_view()
        elif mode == "compact":
            self._show_compact_view()
    
    def _clear_content(self):
        """Clear the content area"""
        for widget in self.components['content_inner'].winfo_children():
            widget.destroy()
        for crop in list(self.temp_crops):
            try:
                if os.path.exists(crop):
                    os.remove(crop)
            except Exception:
                pass
        self.temp_crops.clear()
        self.photo_images.clear()
    
    def _prepare_folder_grouped_data(self):
        """Prepare data grouped by folders"""
        from collections import defaultdict
        folder_groups = defaultdict(list)
        for photo in self.photos_data:
            folder_path = os.path.dirname(photo['path'])
            folder_groups[folder_path].append(photo)
        sorted_folders = []
        for folder_path in sorted(folder_groups.keys()):
            folder_name = os.path.basename(folder_path) if folder_path else "Root"
            photos_in_folder = sorted(folder_groups[folder_path], key=lambda x: x['date_taken'] or '', reverse=True)
            if folder_path not in self.folder_states:
                # Collapse folders by default on first load
                self.folder_states[folder_path] = False
            sorted_folders.append({
                'folder_path': folder_path,
                'folder_name': folder_name,
                'photos': photos_in_folder,
                'photo_count': len(photos_in_folder)
            })
        return sorted_folders
    
    def _create_folder_header(self, parent, folder_info, current_row, col_count, view_mode):
        """Create a folder header with expand/collapse and bulk operations"""
        folder_header_frame = ttk.Frame(parent)
        folder_header_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(10, 5))
        folder_header_frame.configure(relief='raised', borderwidth=1)

        def open_bulk_link_dialog():
            # Bulk tagging dialog: add selected tag to all photos in this folder (pending changes only)
            popup = tk.Toplevel(self.main_frame)
            popup.title("Bulk Link Tags to Folder")
            popup.transient(self.main_frame)
            popup.grab_set()
            popup.geometry("650x500")

            top_frame = ttk.Frame(popup, padding="8")
            top_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
            status_frame = ttk.Frame(popup, padding="8")
            status_frame.grid(row=1, column=0, sticky=(tk.W, tk.E))
            bottom_frame = ttk.Frame(popup, padding="8")
            # Place bottom frame below the list to keep actions at the bottom
            bottom_frame.grid(row=4, column=0, sticky=(tk.W, tk.E))
            popup.columnconfigure(0, weight=1)

            ttk.Label(top_frame, text=f"Folder: {folder_info['folder_name']} ({folder_info['photo_count']} photos)").grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=(0,6))
            ttk.Label(top_frame, text="Add tag to all photos:").grid(row=1, column=0, padx=(0, 8), sticky=tk.W)

            bulk_tag_var = tk.StringVar()
            combo = ttk.Combobox(top_frame, textvariable=bulk_tag_var, values=self.existing_tags, width=30, state='readonly')
            combo.grid(row=1, column=1, padx=(0, 8), sticky=(tk.W, tk.E))
            combo.focus_set()

            result_var = tk.StringVar(value="")
            ttk.Label(status_frame, textvariable=result_var, foreground="gray").grid(row=0, column=0, sticky=tk.W)

            def add_bulk_tag():
                tag_name = bulk_tag_var.get().strip()
                if not tag_name:
                    return
                # Resolve or create tag id
                # Case-insensitive tag lookup
                normalized_tag_name = tag_name.lower().strip()
                if normalized_tag_name in self.tag_name_to_id:
                    tag_id = self.tag_name_to_id[normalized_tag_name]
                else:
                    # Create new tag using database method
                    tag_id = self.db.add_tag(tag_name)
                    if tag_id:
                        self.tag_name_to_id[normalized_tag_name] = tag_id
                        self.tag_id_to_name[tag_id] = tag_name
                        if tag_name not in self.existing_tags:
                            self.existing_tags.append(tag_name)
                            self.existing_tags.sort()
                    else:
                        return  # Failed to create tag

                affected = 0
                for photo in folder_info.get('photos', []):
                    photo_id = photo['id']
                    saved_types = self._get_saved_tag_types_for_photo(photo_id)
                    existing_tag_ids = list(saved_types.keys())
                    pending_tag_ids = self.pending_tag_changes.get(photo_id, [])
                    # Case 1: not present anywhere → add as pending bulk
                    if tag_id not in existing_tag_ids and tag_id not in pending_tag_ids:
                        if photo_id not in self.pending_tag_changes:
                            self.pending_tag_changes[photo_id] = []
                        self.pending_tag_changes[photo_id].append(tag_id)
                        if photo_id not in self.pending_tag_linkage_type:
                            self.pending_tag_linkage_type[photo_id] = {}
                        self.pending_tag_linkage_type[photo_id][tag_id] = 1
                        affected += 1
                    # Case 2: already pending as single → upgrade pending type to bulk
                    elif tag_id in pending_tag_ids:
                        if photo_id not in self.pending_tag_linkage_type:
                            self.pending_tag_linkage_type[photo_id] = {}
                        prev_type = self.pending_tag_linkage_type[photo_id].get(tag_id)
                        if prev_type != 1:
                            self.pending_tag_linkage_type[photo_id][tag_id] = 1
                            affected += 1
                    # Case 3: saved as single → schedule an upgrade by adding to pending and setting type to bulk
                    elif saved_types.get(tag_id) == 0:
                        if photo_id not in self.pending_tag_changes:
                            self.pending_tag_changes[photo_id] = []
                        if tag_id not in self.pending_tag_changes[photo_id]:
                            self.pending_tag_changes[photo_id].append(tag_id)
                        if photo_id not in self.pending_tag_linkage_type:
                            self.pending_tag_linkage_type[photo_id] = {}
                        self.pending_tag_linkage_type[photo_id][tag_id] = 1
                        affected += 1
                    # Case 4: saved as bulk → nothing to do

                self._update_save_button_text()
                # Refresh main view to reflect updated pending tags in each row
                self._switch_view_mode(view_mode)
                result_var.set(f"Added pending tag to {affected} photo(s)")
                bulk_tag_var.set("")
                # Refresh the bulk list to immediately reflect pending adds
                try:
                    refresh_bulk_tag_list()
                except Exception:
                    pass

            ttk.Button(top_frame, text="Add", command=add_bulk_tag).grid(row=1, column=2)

            # Section: Remove bulk-linked tags across this folder
            ttk.Separator(status_frame, orient='horizontal').grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(8, 6))

            list_frame = ttk.Frame(popup, padding="8")
            list_frame.grid(row=3, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
            popup.rowconfigure(3, weight=1)
            list_canvas = tk.Canvas(list_frame, highlightthickness=0)
            list_scroll = ttk.Scrollbar(list_frame, orient="vertical", command=list_canvas.yview)
            list_inner = ttk.Frame(list_canvas)
            list_canvas.create_window((0, 0), window=list_inner, anchor="nw")
            list_canvas.configure(yscrollcommand=list_scroll.set)
            list_canvas.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
            list_scroll.grid(row=0, column=1, sticky=(tk.N, tk.S))
            list_frame.columnconfigure(0, weight=1)
            list_frame.rowconfigure(0, weight=1)
            list_inner.bind("<Configure>", lambda e: list_canvas.configure(scrollregion=list_canvas.bbox("all")))

            bulk_tag_vars: Dict[int, tk.BooleanVar] = {}

            def refresh_bulk_tag_list():
                for child in list(list_inner.winfo_children()):
                    child.destroy()
                bulk_tag_vars.clear()
                # Aggregate bulk-linked tags across all photos in this folder
                tag_id_counts: Dict[int, int] = {}
                pending_add_counts: Dict[int, int] = {}
                for photo in folder_info.get('photos', []):
                    saved_types = self._get_saved_tag_types_for_photo(photo['id'])
                    for tid, ltype in saved_types.items():
                        if ltype == 1:
                            tag_id_counts[tid] = tag_id_counts.get(tid, 0) + 1
                    # Count pending additions (bulk type) for this photo
                    for tid in self.pending_tag_changes.get(photo['id'], []):
                        if self.pending_tag_linkage_type.get(photo['id'], {}).get(tid) == 1:
                            pending_add_counts[tid] = pending_add_counts.get(tid, 0) + 1
                # Include tags that exist only in pending adds
                all_tag_ids = set(tag_id_counts.keys()) | set(pending_add_counts.keys())
                if not all_tag_ids:
                    ttk.Label(list_inner, text="No bulk-linked tags in this folder", foreground="gray").pack(anchor=tk.W, pady=5)
                    return
                for tid in sorted(all_tag_ids, key=lambda x: self.tag_id_to_name.get(x, "")):
                    row = ttk.Frame(list_inner)
                    row.pack(fill=tk.X, pady=1)
                    var = tk.BooleanVar(value=False)
                    bulk_tag_vars[tid] = var
                    ttk.Checkbutton(row, variable=var).pack(side=tk.LEFT, padx=(0, 6))
                    pend_suffix = " (pending)" if pending_add_counts.get(tid, 0) > 0 else ""
                    ttk.Label(row, text=f"{self.tag_id_to_name.get(tid, 'Unknown')}{pend_suffix}").pack(side=tk.LEFT)

            def remove_selected_bulk_tags():
                selected_tids = [tid for tid, v in bulk_tag_vars.items() if v.get()]
                if not selected_tids:
                    return
                affected = 0
                for photo in folder_info.get('photos', []):
                    photo_id = photo['id']
                    saved_types = self._get_saved_tag_types_for_photo(photo_id)
                    for tid in selected_tids:
                        # If it's pending add (bulk), cancel the pending change
                        if tid in self.pending_tag_changes.get(photo_id, []) and self.pending_tag_linkage_type.get(photo_id, {}).get(tid) == 1:
                            self.pending_tag_changes[photo_id] = [x for x in self.pending_tag_changes[photo_id] if x != tid]
                            if not self.pending_tag_changes[photo_id]:
                                del self.pending_tag_changes[photo_id]
                            if photo_id in self.pending_tag_linkage_type and tid in self.pending_tag_linkage_type[photo_id]:
                                del self.pending_tag_linkage_type[photo_id][tid]
                                if not self.pending_tag_linkage_type[photo_id]:
                                    del self.pending_tag_linkage_type[photo_id]
                            affected += 1
                        # Else if it's a saved bulk linkage, mark for removal
                        elif saved_types.get(tid) == 1:
                            if photo_id not in self.pending_tag_removals:
                                self.pending_tag_removals[photo_id] = []
                            if tid not in self.pending_tag_removals[photo_id]:
                                self.pending_tag_removals[photo_id].append(tid)
                                affected += 1
                self._update_save_button_text()
                self._switch_view_mode(view_mode)
                result_var.set(f"Marked bulk tag removals affecting {affected} linkage(s)")
                refresh_bulk_tag_list()

            ttk.Button(bottom_frame, text="Remove selected bulk tags", command=remove_selected_bulk_tags).pack(side=tk.LEFT)
            ttk.Button(bottom_frame, text="Close", command=popup.destroy).pack(side=tk.RIGHT)
            refresh_bulk_tag_list()

        is_expanded = self.folder_states.get(folder_info['folder_path'], True)
        toggle_text = "▼" if is_expanded else "▶"
        toggle_button = tk.Button(folder_header_frame, text=toggle_text, width=2, height=1,
                                  command=lambda: self._toggle_folder(folder_info['folder_path'], view_mode),
                                  font=("Arial", 8), relief='flat', bd=1)
        toggle_button.pack(side=tk.LEFT, padx=(5, 2), pady=5)

        folder_label = ttk.Label(folder_header_frame, text=f"📁 {folder_info['folder_name']} ({folder_info['photo_count']} photos)", font=("Arial", 11, "bold"))
        folder_label.pack(side=tk.LEFT, padx=(0, 10), pady=5)

        # Bulk linkage icon (applies selected tag to all photos in this folder)
        bulk_link_btn = tk.Button(folder_header_frame, text="🔗", width=2, command=open_bulk_link_dialog)
        bulk_link_btn.pack(side=tk.LEFT, padx=(6, 6))

        # Compute and show bulk tags for this folder
        def compute_folder_bulk_tags() -> str:
            bulk_tag_ids = set()
            # Gather saved bulk tags
            for photo in folder_info.get('photos', []):
                photo_id = photo['id']
                saved_types = self._get_saved_tag_types_for_photo(photo_id)
                for tid, ltype in saved_types.items():
                    if ltype == 1:
                        bulk_tag_ids.add(tid)
            # Include pending bulk adds; exclude pending removals
            for photo in folder_info.get('photos', []):
                photo_id = photo['id']
                for tid in self.pending_tag_changes.get(photo_id, []):
                    if self.pending_tag_linkage_type.get(photo_id, {}).get(tid) == 1:
                        if tid not in self.pending_tag_removals.get(photo_id, []):
                            bulk_tag_ids.add(tid)
                # Exclude any saved bulk tags marked for removal
                for tid in self.pending_tag_removals.get(photo_id, []):
                    if tid in bulk_tag_ids:
                        bulk_tag_ids.discard(tid)
            names = [self.tag_id_to_name.get(tid, f"Unknown {tid}") for tid in sorted(bulk_tag_ids, key=lambda x: self.tag_id_to_name.get(x, ""))]
            return ", ".join(names) if names else "None"

        # Append bulk tags to the folder label so it's clearly visible
        try:
            tags_str = compute_folder_bulk_tags()
            if tags_str and tags_str != "None":
                folder_label.configure(text=f"📁 {folder_info['folder_name']} ({folder_info['photo_count']} photos) — Tags: {tags_str}")
            else:
                folder_label.configure(text=f"📁 {folder_info['folder_name']} ({folder_info['photo_count']} photos)")
        except Exception:
            pass

        return folder_header_frame
    
    def _toggle_folder(self, folder_path, view_mode):
        """Toggle folder expand/collapse state"""
        self.folder_states[folder_path] = not self.folder_states.get(folder_path, True)
        self._switch_view_mode(view_mode)
    
    def _create_tag_buttons_frame(self, parent, photo_id, photo_tags, existing_tags, use_grid=False, row=0, col=0):
        """Create tag management buttons for a photo"""
        tags_frame = ttk.Frame(parent)
        existing_tags_list = self.tag_manager.parse_tags_string(photo_tags)
        pending_tag_names = [self.tag_id_to_name.get(tid, f"Unknown {tid}") for tid in self.pending_tag_changes.get(photo_id, [])]
        pending_removal_names = [self.tag_id_to_name.get(tid, f"Unknown {tid}") for tid in self.pending_tag_removals.get(photo_id, [])]
        all_tags = existing_tags_list + pending_tag_names
        unique_tags = self.tag_manager.deduplicate_tags(all_tags)
        unique_tags = [tag for tag in unique_tags if tag not in pending_removal_names]
        current_display = ", ".join(unique_tags) if unique_tags else "None"
        tags_text = ttk.Label(tags_frame, text=current_display)
        tags_text.pack(side=tk.LEFT)
        add_btn = tk.Button(tags_frame, text="🔗", width=2, command=self._create_add_tag_handler(photo_id, tags_text, photo_tags, existing_tags, 0))
        add_btn.pack(side=tk.LEFT, padx=(6, 0))
        if use_grid:
            tags_frame.grid(row=row, column=col, padx=5, sticky=tk.W)
        else:
            tags_frame.pack(side=tk.LEFT, padx=5)
        return tags_frame
    
    def _create_add_tag_handler(self, photo_id, label_widget, photo_tags, available_tags, allowed_delete_type: int = 0):
        """Create a tag management dialog handler for a photo"""
        def handler():
            popup = tk.Toplevel(self.main_frame)
            popup.title("Manage Photo Tags")
            popup.transient(self.main_frame)
            popup.grab_set()
            popup.geometry("600x500")
            popup.resizable(True, True)
            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="Add tag:").grid(row=0, column=0, padx=(0, 8), sticky=tk.W)
            tag_var = tk.StringVar()
            combo = ttk.Combobox(top_frame, textvariable=tag_var, values=available_tags, width=30, state='readonly')
            combo.grid(row=0, column=1, padx=(0, 8), sticky=(tk.W, tk.E))
            combo.focus_set()

            def _update_tags_label_in_main_list():
                # Recompute combined display for the main list label (saved + pending minus removals)
                existing_tags_list = self.tag_manager.parse_tags_string(photo_tags)
                pending_tag_names = [self.tag_id_to_name.get(tid, f"Unknown {tid}") for tid in self.pending_tag_changes.get(photo_id, [])]
                pending_removal_names = [self.tag_id_to_name.get(tid, f"Unknown {tid}") for tid in self.pending_tag_removals.get(photo_id, [])]
                all_tags = existing_tags_list + pending_tag_names
                unique_tags = self.tag_manager.deduplicate_tags(all_tags)
                # Remove tags marked for removal from display
                unique_tags = [tag for tag in unique_tags if tag not in pending_removal_names]
                current_tags = ", ".join(unique_tags) if unique_tags else "None"
                try:
                    label_widget.configure(text=current_tags)
                except Exception:
                    pass

            def add_selected_tag():
                tag_name = tag_var.get().strip()
                if not tag_name:
                    return
                # Case-insensitive tag lookup
                normalized_tag_name = tag_name.lower().strip()
                if normalized_tag_name in self.tag_name_to_id:
                    tag_id = self.tag_name_to_id[normalized_tag_name]
                else:
                    # Create new tag using database method
                    tag_id = self.db.add_tag(tag_name)
                    if tag_id:
                        self.tag_name_to_id[normalized_tag_name] = tag_id
                        self.tag_id_to_name[tag_id] = tag_name
                        if tag_name not in self.existing_tags:
                            self.existing_tags.append(tag_name)
                            self.existing_tags.sort()
                    else:
                        return  # Failed to create tag
                saved_types = self._get_saved_tag_types_for_photo(photo_id)
                existing_tag_ids = list(saved_types.keys())
                pending_tag_ids = self.pending_tag_changes.get(photo_id, [])
                all_existing_tag_ids = existing_tag_ids + pending_tag_ids
                if tag_id not in all_existing_tag_ids:
                    if photo_id not in self.pending_tag_changes:
                        self.pending_tag_changes[photo_id] = []
                    self.pending_tag_changes[photo_id].append(tag_id)
                    # mark pending type as single (0)
                    if photo_id not in self.pending_tag_linkage_type:
                        self.pending_tag_linkage_type[photo_id] = {}
                    self.pending_tag_linkage_type[photo_id][tag_id] = 0
                    refresh_tag_list()
                    self._update_save_button_text()
                    _update_tags_label_in_main_list()
                tag_var.set("")

            ttk.Button(top_frame, text="Add", command=add_selected_tag).grid(row=0, column=2, padx=(0, 8))

            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: Dict[str, tk.BooleanVar] = {}

            def refresh_tag_list():
                for widget in scrollable_frame.winfo_children():
                    widget.destroy()
                selected_tag_vars.clear()
                saved_types = self._get_saved_tag_types_for_photo(photo_id)
                existing_tag_ids = list(saved_types.keys())
                pending_tag_ids = self.pending_tag_changes.get(photo_id, [])
                pending_removal_ids = self.pending_tag_removals.get(photo_id, [])
                all_tag_ids = existing_tag_ids + pending_tag_ids
                unique_tag_ids = list(set(all_tag_ids))
                unique_tag_ids = [tid for tid in unique_tag_ids if tid not in pending_removal_ids]
                unique_tag_names = [self.tag_id_to_name.get(tid, f"Unknown {tid}") for tid in unique_tag_ids]
                if not unique_tag_names:
                    ttk.Label(scrollable_frame, text="No tags linked to this photo", foreground="gray").pack(anchor=tk.W, pady=5)
                    return
                for i, tag_id in enumerate(unique_tag_ids):
                    tag_name = self.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)
                    is_pending = tag_id in pending_tag_ids
                    saved_type = saved_types.get(tag_id)
                    pending_type = self.pending_tag_linkage_type.get(photo_id, {}).get(tag_id)
                    # In single-photo dialog, only allow selecting pending if pending type is single (0)
                    is_pending_single = is_pending and pending_type == 0
                    can_select = is_pending_single or (saved_type == allowed_delete_type)
                    cb = ttk.Checkbutton(frame, variable=var)
                    if not can_select:
                        try:
                            cb.state(["disabled"])  # disable selection for disallowed types
                        except Exception:
                            pass
                    cb.pack(side=tk.LEFT, padx=(0, 5))
                    type_label = 'single' if saved_type == 0 else ('bulk' if saved_type == 1 else '?')
                    pending_label = "pending bulk" if (is_pending and pending_type == 1) else "pending"
                    status_text = f" ({pending_label})" if is_pending else f" (saved {type_label})"
                    status_color = "blue" if is_pending else ("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 = []
                for tag_name, var in selected_tag_vars.items():
                    if var.get() and tag_name in self.tag_name_to_id:
                        tag_ids_to_remove.append(self.tag_name_to_id[tag_name])
                if not tag_ids_to_remove:
                    return
                if photo_id in self.pending_tag_changes:
                    self.pending_tag_changes[photo_id] = [tid for tid in self.pending_tag_changes[photo_id] if tid not in tag_ids_to_remove]
                    if not self.pending_tag_changes[photo_id]:
                        del self.pending_tag_changes[photo_id]
                saved_types = self._get_saved_tag_types_for_photo(photo_id)
                for tag_id in tag_ids_to_remove:
                    if saved_types.get(tag_id) == allowed_delete_type:
                        if photo_id not in self.pending_tag_removals:
                            self.pending_tag_removals[photo_id] = []
                        if tag_id not in self.pending_tag_removals[photo_id]:
                            self.pending_tag_removals[photo_id].append(tag_id)
                refresh_tag_list()
                self._update_save_button_text()
                _update_tags_label_in_main_list()

            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=popup.destroy).pack(side=tk.RIGHT)
            refresh_tag_list()

            def on_close():
                existing_tags_list = self.tag_manager.parse_tags_string(photo_tags)
                pending_tag_names = [self.tag_id_to_name.get(tid, f"Unknown {tid}") for tid in self.pending_tag_changes.get(photo_id, [])]
                pending_removal_names = [self.tag_id_to_name.get(tid, f"Unknown {tid}") for tid in self.pending_tag_removals.get(photo_id, [])]
                all_tags = existing_tags_list + pending_tag_names
                unique_tags = self.tag_manager.deduplicate_tags(all_tags)
                unique_tags = [tag for tag in unique_tags if tag not in pending_removal_names]
                current_tags = ", ".join(unique_tags) if unique_tags else "None"
                label_widget.configure(text=current_tags)
                popup.destroy()

            popup.protocol("WM_DELETE_WINDOW", on_close)
        return handler
    
    def _show_list_view(self):
        """Show the list view"""
        self._clear_content()
        self.current_visible_cols = [col.copy() for col in self.column_config['list'] if self.column_visibility['list'][col['key']]]
        col_count = len(self.current_visible_cols)
        if col_count == 0:
            ttk.Label(self.components['content_inner'], text="No columns visible. Right-click to show columns.", 
                     font=("Arial", 12), foreground="gray").grid(row=0, column=0, pady=20)
            return
        
        # Configure columns
        for i, col in enumerate(self.current_visible_cols):
            self.components['content_inner'].columnconfigure(i, weight=col['weight'], minsize=col['width'])
        
        # Create header with resizing separators
        header_frame = ttk.Frame(self.components['content_inner'])
        header_frame.grid(row=0, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
        for i, col in enumerate(self.current_visible_cols):
            header_frame.columnconfigure(i * 2, weight=col['weight'], minsize=col['width'])
            if i < len(self.current_visible_cols) - 1:
                header_frame.columnconfigure(i * 2 + 1, weight=0, minsize=1)
        
        for i, col in enumerate(self.current_visible_cols):
            header_label = ttk.Label(header_frame, text=col['label'], font=("Arial", 10, "bold"))
            header_label.grid(row=0, column=i * 2, padx=5, sticky=tk.W)
            header_label.bind("<Button-3>", lambda e, mode='list': self._show_column_context_menu(e, mode))
            
            # Add resizing separator between columns
            if i < len(self.current_visible_cols) - 1:
                separator_frame = tk.Frame(header_frame, width=10, bg='red', cursor="sb_h_double_arrow")
                separator_frame.grid(row=0, column=i * 2 + 1, sticky='ns', padx=0)
                separator_frame.grid_propagate(False)
                inner_line = tk.Frame(separator_frame, bg='darkred', width=2)
                inner_line.pack(fill=tk.Y, expand=True)
                
                # Bind resize events
                separator_frame.bind("<Button-1>", lambda e, col_idx=i: self._start_resize(e, col_idx))
                separator_frame.bind("<B1-Motion>", lambda e, col_idx=i: self._do_resize(e, col_idx))
                separator_frame.bind("<ButtonRelease-1>", self._stop_resize)
                separator_frame.bind("<Enter>", lambda e, f=separator_frame, l=inner_line: (f.configure(bg='orange'), l.configure(bg='darkorange')))
                separator_frame.bind("<Leave>", lambda e, f=separator_frame, l=inner_line: (f.configure(bg='red'), l.configure(bg='darkred')))
                
                inner_line.bind("<Button-1>", lambda e, col_idx=i: self._start_resize(e, col_idx))
                inner_line.bind("<B1-Motion>", lambda e, col_idx=i: self._do_resize(e, col_idx))
                inner_line.bind("<ButtonRelease-1>", self._stop_resize)
        
        header_frame.bind("<Button-3>", lambda e, mode='list': self._show_column_context_menu(e, mode))
        ttk.Separator(self.components['content_inner'], orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
        
        # Create folder grouped data and display
        folder_data = self._prepare_folder_grouped_data()
        current_row = 2
        for folder_info in folder_data:
            self._create_folder_header(self.components['content_inner'], folder_info, current_row, col_count, 'list')
            current_row += 1
            if self.folder_states.get(folder_info['folder_path'], True):
                for photo in folder_info['photos']:
                    row_frame = ttk.Frame(self.components['content_inner'])
                    row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=1)
                    for i, col in enumerate(self.current_visible_cols):
                        row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
                    
                    for i, col in enumerate(self.current_visible_cols):
                        key = col['key']
                        if key == 'id':
                            text = str(photo['id'])
                        elif key == 'filename':
                            text = photo['filename']
                        elif key == 'path':
                            text = photo['path']
                        elif key == 'processed':
                            text = "Yes" if photo['processed'] else "No"
                        elif key == 'date_taken':
                            text = photo['date_taken'] or "Unknown"
                        elif key == 'faces':
                            text = str(photo['face_count'])
                        elif key == 'tags':
                            self._create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], self.existing_tags, use_grid=True, row=0, col=i)
                            continue
                        
                        # Render text wrapped to header width
                        if key == 'filename':
                            lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", 
                                          wraplength=col['width'], anchor='w', justify='left')
                            lbl.grid(row=0, column=i, padx=5, sticky=tk.W)
                            lbl.bind("<Button-1>", lambda e, p=photo['path']: self._open_photo(p))
                        elif key == 'faces' and photo['face_count'] > 0:
                            lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", 
                                          wraplength=col['width'], anchor='w', justify='left')
                            lbl.grid(row=0, column=i, padx=5, sticky=tk.W)
                            # Show people names in tooltip on hover
                            try:
                                people_tooltip_text = self._get_people_names_for_photo(photo['id'])
                                self._create_tooltip(lbl, people_tooltip_text)
                            except Exception:
                                pass
                        else:
                            ttk.Label(row_frame, text=text, wraplength=col['width'], anchor='w', justify='left').grid(row=0, column=i, padx=5, sticky=tk.W)
                    current_row += 1
    
    def _show_icon_view(self):
        """Show the icon view"""
        self._clear_content()
        visible_cols = [col for col in self.column_config['icons'] if self.column_visibility['icons'][col['key']]]
        col_count = len(visible_cols)
        if col_count == 0:
            ttk.Label(self.components['content_inner'], text="No columns visible. Right-click to show columns.", 
                     font=("Arial", 12), foreground="gray").grid(row=0, column=0, pady=20)
            return
        
        # Configure columns
        for i, col in enumerate(visible_cols):
            self.components['content_inner'].columnconfigure(i, weight=col['weight'], minsize=col['width'])
        
        # Create header
        header_frame = ttk.Frame(self.components['content_inner'])
        header_frame.grid(row=0, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
        for i, col in enumerate(visible_cols):
            header_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
        
        for i, col in enumerate(visible_cols):
            header_label = ttk.Label(header_frame, text=col['label'], font=("Arial", 10, "bold"))
            header_label.grid(row=0, column=i, padx=5, sticky=tk.W)
            header_label.bind("<Button-3>", lambda e, mode='icons': self._show_column_context_menu(e, mode))
        
        header_frame.bind("<Button-3>", lambda e, mode='icons': self._show_column_context_menu(e, mode))
        ttk.Separator(self.components['content_inner'], orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
        
        # Create folder grouped data and display
        folder_data = self._prepare_folder_grouped_data()
        current_row = 2
        for folder_info in folder_data:
            self._create_folder_header(self.components['content_inner'], folder_info, current_row, col_count, 'icons')
            current_row += 1
            if self.folder_states.get(folder_info['folder_path'], True):
                for photo in folder_info['photos']:
                    row_frame = ttk.Frame(self.components['content_inner'])
                    row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=2)
                    for i, col in enumerate(visible_cols):
                        row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
                    
                    col_idx = 0
                    for col in visible_cols:
                        key = col['key']
                        if key == 'thumbnail':
                            thumbnail_frame = ttk.Frame(row_frame)
                            thumbnail_frame.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                            try:
                                if os.path.exists(photo['path']):
                                    img = Image.open(photo['path'])
                                    img.thumbnail((150, 150), Image.Resampling.LANCZOS)
                                    photo_img = ImageTk.PhotoImage(img)
                                    self.photo_images.append(photo_img)
                                    canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg='lightgray', highlightthickness=0)
                                    canvas.pack()
                                    canvas.create_image(75, 75, image=photo_img)
                                else:
                                    canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg='lightgray', highlightthickness=0)
                                    canvas.pack()
                                    canvas.create_text(75, 75, text="🖼️", fill="gray", font=("Arial", 24))
                            except Exception:
                                canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg='lightgray', highlightthickness=0)
                                canvas.pack()
                                canvas.create_text(75, 75, text="❌", fill="red", font=("Arial", 24))
                        else:
                            if key == 'id':
                                text = str(photo['id'])
                            elif key == 'filename':
                                text = photo['filename']
                            elif key == 'processed':
                                text = "Yes" if photo['processed'] else "No"
                            elif key == 'date_taken':
                                text = photo['date_taken'] or "Unknown"
                            elif key == 'faces':
                                text = str(photo['face_count'])
                            elif key == 'tags':
                                self._create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], self.existing_tags, use_grid=True, row=0, col=col_idx)
                                col_idx += 1
                                continue
                            
                            # Render text wrapped to header width
                            if key == 'filename':
                                lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", 
                                              wraplength=col['width'], anchor='w', justify='left')
                                lbl.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                                lbl.bind("<Button-1>", lambda e, p=photo['path']: self._open_photo(p))
                            elif key == 'faces' and photo['face_count'] > 0:
                                lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", 
                                              wraplength=col['width'], anchor='w', justify='left')
                                lbl.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                                # Show people names in tooltip on hover
                                try:
                                    people_tooltip_text = self._get_people_names_for_photo(photo['id'])
                                    self._create_tooltip(lbl, people_tooltip_text)
                                except Exception:
                                    pass
                            else:
                                ttk.Label(row_frame, text=text, wraplength=col['width'], anchor='w', justify='left').grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                        col_idx += 1
                    current_row += 1
    
    def _show_compact_view(self):
        """Show the compact view"""
        self._clear_content()
        visible_cols = [col for col in self.column_config['compact'] if self.column_visibility['compact'][col['key']]]
        col_count = len(visible_cols)
        if col_count == 0:
            ttk.Label(self.components['content_inner'], text="No columns visible. Right-click to show columns.", 
                     font=("Arial", 12), foreground="gray").grid(row=0, column=0, pady=20)
            return
        
        # Configure columns
        for i, col in enumerate(visible_cols):
            self.components['content_inner'].columnconfigure(i, weight=col['weight'], minsize=col['width'])
        
        # Create header
        header_frame = ttk.Frame(self.components['content_inner'])
        header_frame.grid(row=0, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
        for i, col in enumerate(visible_cols):
            header_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
        
        for i, col in enumerate(visible_cols):
            header_label = ttk.Label(header_frame, text=col['label'], font=("Arial", 10, "bold"))
            header_label.grid(row=0, column=i, padx=5, sticky=tk.W)
            header_label.bind("<Button-3>", lambda e, mode='compact': self._show_column_context_menu(e, mode))
        
        header_frame.bind("<Button-3>", lambda e, mode='compact': self._show_column_context_menu(e, mode))
        ttk.Separator(self.components['content_inner'], orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
        
        # Create folder grouped data and display
        folder_data = self._prepare_folder_grouped_data()
        current_row = 2
        for folder_info in folder_data:
            self._create_folder_header(self.components['content_inner'], folder_info, current_row, col_count, 'compact')
            current_row += 1
            if self.folder_states.get(folder_info['folder_path'], True):
                for photo in folder_info['photos']:
                    row_frame = ttk.Frame(self.components['content_inner'])
                    row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=1)
                    for i, col in enumerate(visible_cols):
                        row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
                    
                    col_idx = 0
                    for col in visible_cols:
                        key = col['key']
                        if key == 'filename':
                            text = photo['filename']
                        elif key == 'faces':
                            text = str(photo['face_count'])
                        elif key == 'tags':
                            self._create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], self.existing_tags, use_grid=True, row=0, col=col_idx)
                            col_idx += 1
                            continue
                        
                        # Render text wrapped to header width
                        if key == 'filename':
                            lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", 
                                          wraplength=col['width'], anchor='w', justify='left')
                            lbl.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                            lbl.bind("<Button-1>", lambda e, p=photo['path']: self._open_photo(p))
                        elif key == 'faces' and photo['face_count'] > 0:
                            lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", 
                                          wraplength=col['width'], anchor='w', justify='left')
                            lbl.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                            # Show people names in tooltip on hover
                            try:
                                people_tooltip_text = self._get_people_names_for_photo(photo['id'])
                                self._create_tooltip(lbl, people_tooltip_text)
                            except Exception:
                                pass
                        else:
                            ttk.Label(row_frame, text=text, wraplength=col['width'], anchor='w', justify='left').grid(row=0, column=col_idx, padx=5, sticky=tk.W)
                        col_idx += 1
                    current_row += 1
    
    def _create_tooltip(self, widget, text: str):
        """Create a simple tooltip for 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 activate(self):
        """Activate the panel"""
        self.is_active = True
        # Reload photos data when activating the panel
        self._load_existing_tags()
        self._load_photos()
        self._switch_view_mode(self.view_mode_var.get())
        # Rebind mousewheel scrolling when activated
        self._bind_mousewheel_scrolling()
    
    def deactivate(self):
        """Deactivate the panel"""
        if self.is_active:
            self._cleanup()
            self.is_active = False
    
    def _cleanup(self):
        """Clean up resources"""
        # Unbind mousewheel scrolling
        self._unbind_mousewheel_scrolling()
        
        # Clean up 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()
        
        # Clear photo images
        self.photo_images.clear()
        
        # Clear state
        self.pending_tag_changes.clear()
        self.pending_tag_removals.clear()
        self.pending_tag_linkage_type.clear()
        self.folder_states.clear()
    
    def update_layout(self):
        """Update panel layout for responsiveness"""
        if hasattr(self, 'components') and 'content_canvas' in self.components:
            # Update content canvas scroll region
            canvas = self.components['content_canvas']
            canvas.update_idletasks()
            canvas.configure(scrollregion=canvas.bbox("all"))
